mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into extensions-api
This commit is contained in:
commit
0f57ea91b1
56 files changed
+815
-616
No files matched your search
@@ -4,7 +4,7 @@ import { Input } from "../../input";
|
||||
import { observable, autorun } from "mobx";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { isRequired } from "../../input/input.validators";
|
||||
import { isRequired } from "../../input/input_validators";
|
||||
|
||||
interface Props {
|
||||
cluster: Cluster;
|
||||
@@ -33,7 +33,7 @@ export class ClusterNameSetting extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="Cluster Name"/>
|
||||
<SubTitle title="Cluster Name" />
|
||||
<p>Define cluster name.</p>
|
||||
<Input
|
||||
theme="round-black"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { observable, autorun } from "mobx";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { Cluster } from "../../../../main/cluster";
|
||||
import { Input } from "../../input";
|
||||
import { isUrl } from "../../input/input.validators";
|
||||
import { isUrl } from "../../input/input_validators";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
|
||||
interface Props {
|
||||
@@ -33,7 +33,7 @@ export class ClusterProxySetting extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="HTTP Proxy"/>
|
||||
<SubTitle title="HTTP Proxy" />
|
||||
<p>HTTP Proxy server. Used for communicating with Kubernetes API.</p>
|
||||
<Input
|
||||
theme="round-black"
|
||||
|
||||
@@ -14,14 +14,15 @@ import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import { clusterStore } from "./cluster.store";
|
||||
import { eventStore } from "../+events/event.store";
|
||||
import { isAllowedResource } from "../../../common/rbac";
|
||||
import { getHostedCluster } from "../../../common/cluster-store";
|
||||
|
||||
@observer
|
||||
export class Cluster extends React.Component {
|
||||
private dependentStores = [nodesStore, podsStore];
|
||||
|
||||
private watchers = [
|
||||
interval(60, () => clusterStore.getMetrics()),
|
||||
interval(20, () => eventStore.loadAll())
|
||||
interval(60, () => { getHostedCluster().available && clusterStore.getMetrics()}),
|
||||
interval(20, () => { getHostedCluster().available && eventStore.loadAll()})
|
||||
];
|
||||
|
||||
@computed get isLoaded() {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { _i18n } from "../../i18n";
|
||||
import { Dialog, DialogProps } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { Input } from "../input";
|
||||
import { systemName } from "../input/input.validators";
|
||||
import { systemName } from "../input/input_validators";
|
||||
import { IResourceQuotaValues, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
|
||||
import { Select } from "../select";
|
||||
import { Icon } from "../icon";
|
||||
@@ -73,7 +73,7 @@ export class AddQuotaDialog extends React.Component<Props> {
|
||||
const isCount = quota.startsWith("count/");
|
||||
const icon = isCompute ? "memory" : isStorage ? "storage" : isCount ? "looks_one" : "";
|
||||
return {
|
||||
label: icon ? <span className="nobr"><Icon material={icon}/> {quota}</span> : quota,
|
||||
label: icon ? <span className="nobr"><Icon material={icon} /> {quota}</span> : quota,
|
||||
value: quota,
|
||||
};
|
||||
});
|
||||
@@ -151,7 +151,7 @@ export class AddQuotaDialog extends React.Component<Props> {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SubTitle title={<Trans>Namespace</Trans>}/>
|
||||
<SubTitle title={<Trans>Namespace</Trans>} />
|
||||
<NamespaceSelect
|
||||
value={this.namespace}
|
||||
placeholder={_i18n._(t`Namespace`)}
|
||||
@@ -160,7 +160,7 @@ export class AddQuotaDialog extends React.Component<Props> {
|
||||
onChange={({ value }) => this.namespace = value}
|
||||
/>
|
||||
|
||||
<SubTitle title={<Trans>Values</Trans>}/>
|
||||
<SubTitle title={<Trans>Values</Trans>} />
|
||||
<div className="flex gaps align-center">
|
||||
<Select
|
||||
className="quota-select"
|
||||
@@ -191,7 +191,7 @@ export class AddQuotaDialog extends React.Component<Props> {
|
||||
<div key={quota} className="quota flex gaps inline align-center">
|
||||
<div className="name">{quota}</div>
|
||||
<div className="value">{value}</div>
|
||||
<Icon material="clear" onClick={() => this.quotas[quota] = ""}/>
|
||||
<Icon material="clear" onClick={() => this.quotas[quota] = ""} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { _i18n } from "../../i18n";
|
||||
import { Dialog, DialogProps } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { Input } from "../input";
|
||||
import { systemName } from "../input/input.validators";
|
||||
import { systemName } from "../input/input_validators";
|
||||
import { Secret, secretsApi, SecretType } from "../../api/endpoints";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { NamespaceSelect } from "../+namespaces/namespace-select";
|
||||
@@ -184,7 +184,7 @@ export class AddSecretDialog extends React.Component<Props> {
|
||||
<Wizard header={header} done={this.close}>
|
||||
<WizardStep contentClass="flow column" nextLabel={<Trans>Create</Trans>} next={this.createSecret}>
|
||||
<div className="secret-name">
|
||||
<SubTitle title={<Trans>Secret name</Trans>}/>
|
||||
<SubTitle title={<Trans>Secret name</Trans>} />
|
||||
<Input
|
||||
autoFocus required
|
||||
placeholder={_i18n._(t`Name`)}
|
||||
@@ -194,7 +194,7 @@ export class AddSecretDialog extends React.Component<Props> {
|
||||
</div>
|
||||
<div className="flex auto gaps">
|
||||
<div className="secret-namespace">
|
||||
<SubTitle title={<Trans>Namespace</Trans>}/>
|
||||
<SubTitle title={<Trans>Namespace</Trans>} />
|
||||
<NamespaceSelect
|
||||
themeName="light"
|
||||
value={namespace}
|
||||
@@ -202,7 +202,7 @@ export class AddSecretDialog extends React.Component<Props> {
|
||||
/>
|
||||
</div>
|
||||
<div className="secret-type">
|
||||
<SubTitle title={<Trans>Secret type</Trans>}/>
|
||||
<SubTitle title={<Trans>Secret type</Trans>} />
|
||||
<Select
|
||||
themeName="light"
|
||||
options={this.types}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Wizard, WizardStep } from "../wizard";
|
||||
import { namespaceStore } from "./namespace.store";
|
||||
import { Namespace } from "../../api/endpoints";
|
||||
import { Input } from "../input";
|
||||
import { systemName } from "../input/input.validators";
|
||||
import { systemName } from "../input/input_validators";
|
||||
import { Notifications } from "../notifications";
|
||||
|
||||
interface Props extends DialogProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { isPath } from '../input/input.validators';
|
||||
import { isPath } from '../input/input_validators';
|
||||
import { Checkbox } from '../checkbox';
|
||||
import { Input } from '../input';
|
||||
import { SubTitle } from '../layout/sub-title';
|
||||
|
||||
+3
-3
@@ -10,7 +10,7 @@ import { Wizard, WizardStep } from "../wizard";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { serviceAccountsStore } from "./service-accounts.store";
|
||||
import { Input } from "../input";
|
||||
import { systemName } from "../input/input.validators";
|
||||
import { systemName } from "../input/input_validators";
|
||||
import { NamespaceSelect } from "../+namespaces/namespace-select";
|
||||
import { Notifications } from "../notifications";
|
||||
import { showDetails } from "../../navigation";
|
||||
@@ -62,14 +62,14 @@ export class CreateServiceAccountDialog extends React.Component<Props> {
|
||||
>
|
||||
<Wizard header={header} done={this.close}>
|
||||
<WizardStep nextLabel={<Trans>Create</Trans>} next={this.createAccount}>
|
||||
<SubTitle title={<Trans>Account Name</Trans>}/>
|
||||
<SubTitle title={<Trans>Account Name</Trans>} />
|
||||
<Input
|
||||
autoFocus required
|
||||
placeholder={_i18n._(t`Enter a name`)}
|
||||
validators={systemName}
|
||||
value={name} onChange={v => this.name = v.toLowerCase()}
|
||||
/>
|
||||
<SubTitle title={<Trans>Namespace</Trans>}/>
|
||||
<SubTitle title={<Trans>Namespace</Trans>} />
|
||||
<NamespaceSelect
|
||||
themeName="light"
|
||||
value={namespace}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { CronJob, cronJobApi, jobApi, Job } from "../../api/endpoints";
|
||||
import { Notifications } from "../notifications";
|
||||
import { cssNames } from "../../utils";
|
||||
import { Input } from "../input";
|
||||
import { systemName, maxLength } from "../input/input.validators";
|
||||
import { systemName, maxLength } from "../input/input_validators";
|
||||
|
||||
interface Props extends Partial<DialogProps> {
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Icon } from "../icon";
|
||||
import { Input } from "../input";
|
||||
import { cssNames, prevDefault } from "../../utils";
|
||||
import { Button } from "../button";
|
||||
import { isRequired, Validator } from "../input/input.validators";
|
||||
import { isRequired, Validator } from "../input/input_validators";
|
||||
|
||||
@observer
|
||||
export class Workspaces extends React.Component {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
.theme-light & {
|
||||
border: 1px solid gainsboro;
|
||||
border-radius: $radius;
|
||||
|
||||
.ace_scrollbar {
|
||||
@include custom-scrollbar(dark);
|
||||
@@ -19,7 +18,6 @@
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
font-size: 90%;
|
||||
border-radius: $radius;
|
||||
}
|
||||
|
||||
// --Theme customization
|
||||
|
||||
@@ -45,8 +45,8 @@ export class App extends React.Component {
|
||||
const clusterId = getHostedClusterId();
|
||||
logger.info(`[APP]: Init dashboard, clusterId=${clusterId}, frameId=${frameId}`)
|
||||
await Terminal.preloadFonts()
|
||||
await clusterIpc.activate.invokeFromRenderer(clusterId, frameId);
|
||||
await getHostedCluster().whenReady; // cluster.refresh() is done at this point
|
||||
await clusterIpc.setFrameId.invokeFromRenderer(clusterId, frameId);
|
||||
await getHostedCluster().whenReady; // cluster.activate() is done at this point
|
||||
}
|
||||
|
||||
get startURL() {
|
||||
|
||||
@@ -18,13 +18,6 @@ export class Badge extends React.Component<Props> {
|
||||
{label}
|
||||
{children}
|
||||
</span>
|
||||
{ /**
|
||||
* This is a zero-width-space. It makes there be a word seperation
|
||||
* between adjacent Badge's because <span>'s are ignored for browers'
|
||||
* word detection algorithmns use for determining the extent of the
|
||||
* text to highlight on multi-click sequences.
|
||||
*/}
|
||||
​
|
||||
</>
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.outlined {
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
|
||||
&.active,
|
||||
&:focus {
|
||||
color: inherit;
|
||||
box-shadow: 0 0 0 1px inset;
|
||||
}
|
||||
}
|
||||
|
||||
&.big {
|
||||
font-size: 2.2 * $unit;
|
||||
border-radius: 50px;
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface ButtonProps extends ButtonHTMLAttributes<any>, TooltipDecorator
|
||||
primary?: boolean;
|
||||
accent?: boolean;
|
||||
plain?: boolean;
|
||||
outlined?: boolean;
|
||||
hidden?: boolean;
|
||||
active?: boolean;
|
||||
big?: boolean;
|
||||
@@ -23,12 +24,12 @@ export class Button extends React.PureComponent<ButtonProps, {}> {
|
||||
private button: HTMLButtonElement;
|
||||
|
||||
render() {
|
||||
const { className, waiting, label, primary, accent, plain, hidden, active, big, round, tooltip, children, ...props } = this.props;
|
||||
const { className, waiting, label, primary, accent, plain, hidden, active, big, round, outlined, tooltip, children, ...props } = this.props;
|
||||
const btnProps = props as Partial<ButtonProps>;
|
||||
if (hidden) return null;
|
||||
|
||||
btnProps.className = cssNames('Button', className, {
|
||||
waiting, primary, accent, plain, active, big, round,
|
||||
waiting, primary, accent, plain, active, big, round, outlined
|
||||
});
|
||||
|
||||
const btnContent: ReactNode = (
|
||||
|
||||
@@ -39,6 +39,7 @@ export class CreateResource extends React.Component<Props> {
|
||||
|
||||
create = async () => {
|
||||
if (this.error) return;
|
||||
if (!this.data.trim()) return; // do not save when field is empty
|
||||
const resources = jsYaml.safeLoadAll(this.data)
|
||||
.filter(v => !!v) // skip empty documents if "---" pasted at the beginning or end
|
||||
const createdResources: string[] = [];
|
||||
@@ -54,12 +55,14 @@ export class CreateResource extends React.Component<Props> {
|
||||
errors.forEach(Notifications.error);
|
||||
if (!createdResources.length) throw errors[0];
|
||||
}
|
||||
return (
|
||||
const successMessage = (
|
||||
<p>
|
||||
<Plural value={createdResources.length} one="Resource" other="Resources"/>{" "}
|
||||
<Trans><b>{createdResources.join(", ")}</b> successfully created</Trans>
|
||||
</p>
|
||||
)
|
||||
Notifications.ok(successMessage)
|
||||
return successMessage
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -67,11 +70,6 @@ export class CreateResource extends React.Component<Props> {
|
||||
const { className } = this.props;
|
||||
return (
|
||||
<div className={cssNames("CreateResource flex column", className)}>
|
||||
<EditorPanel
|
||||
tabId={tabId}
|
||||
value={data}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<InfoPanel
|
||||
tabId={tabId}
|
||||
error={error}
|
||||
@@ -79,6 +77,11 @@ export class CreateResource extends React.Component<Props> {
|
||||
submitLabel={_i18n._(t`Create`)}
|
||||
showNotifications={false}
|
||||
/>
|
||||
<EditorPanel
|
||||
tabId={tabId}
|
||||
value={data}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,10 +14,6 @@
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
|
||||
> .resizer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,24 +61,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.resizer {
|
||||
$height: 12px;
|
||||
|
||||
position: absolute;
|
||||
top: -$height / 2;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 100%;
|
||||
height: $height;
|
||||
cursor: row-resize;
|
||||
z-index: 10;
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.AceEditor {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,8 @@ export class DockStore {
|
||||
];
|
||||
|
||||
protected storage = createStorage("dock", {}); // keep settings in localStorage
|
||||
public defaultTabId = this.initialTabs[0].id;
|
||||
public minHeight = 100;
|
||||
public readonly defaultTabId = this.initialTabs[0].id;
|
||||
public readonly minHeight = 100;
|
||||
|
||||
@observable isOpen = false;
|
||||
@observable fullSize = false;
|
||||
@@ -45,11 +45,12 @@ export class DockStore {
|
||||
}
|
||||
|
||||
get maxHeight() {
|
||||
const mainLayoutHeader = 40;
|
||||
const mainLayoutTabs = 33;
|
||||
const mainLayoutMargin = 16;
|
||||
const dockTabs = 33;
|
||||
return window.innerHeight - mainLayoutHeader - mainLayoutTabs - mainLayoutMargin - dockTabs;
|
||||
const mainLayoutHeader = 40
|
||||
const mainLayoutTabs = 33
|
||||
const mainLayoutMargin = 16
|
||||
const dockTabs = 33
|
||||
const preferedMax = window.innerHeight - mainLayoutHeader - mainLayoutTabs - mainLayoutMargin - dockTabs
|
||||
return Math.max(preferedMax, this.minHeight) // don't let max < min
|
||||
}
|
||||
|
||||
constructor() {
|
||||
@@ -65,7 +66,6 @@ export class DockStore {
|
||||
});
|
||||
|
||||
// adjust terminal height if window size changes
|
||||
this.checkMaxHeight();
|
||||
window.addEventListener("resize", throttle(this.checkMaxHeight, 250));
|
||||
}
|
||||
|
||||
@@ -171,20 +171,19 @@ export class DockStore {
|
||||
|
||||
@action
|
||||
selectTab(tabId: TabId) {
|
||||
const tab = this.getTabById(tabId);
|
||||
this.selectedTabId = tab ? tab.id : null;
|
||||
this.selectedTabId = this.getTabById(tabId)?.id ?? null;
|
||||
}
|
||||
|
||||
@action
|
||||
setHeight(height: number) {
|
||||
this.height = Math.max(0, Math.min(height, this.maxHeight));
|
||||
setHeight(height?: number) {
|
||||
this.height = Math.max(this.minHeight, Math.min(height || this.minHeight, this.maxHeight));
|
||||
}
|
||||
|
||||
@action
|
||||
reset() {
|
||||
this.selectedTabId = this.defaultTabId;
|
||||
this.tabs.replace(this.initialTabs);
|
||||
this.height = this.defaultHeight;
|
||||
this.setHeight(this.defaultHeight);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { autobind, cssNames, prevDefault } from "../../utils";
|
||||
import { Draggable, DraggableState } from "../draggable";
|
||||
import { ResizingAnchor, ResizeDirection } from "../resizing-anchor";
|
||||
import { Icon } from "../icon";
|
||||
import { Tabs } from "../tabs/tabs";
|
||||
import { MenuItem } from "../menu";
|
||||
@@ -29,28 +29,8 @@ interface Props {
|
||||
|
||||
@observer
|
||||
export class Dock extends React.Component<Props> {
|
||||
onResizeStart = () => {
|
||||
const { isOpen, open, setHeight, minHeight } = dockStore;
|
||||
if (!isOpen) {
|
||||
open();
|
||||
setHeight(minHeight);
|
||||
}
|
||||
}
|
||||
|
||||
onResize = ({ offsetY }: DraggableState) => {
|
||||
const { isOpen, close, height, setHeight, minHeight, defaultHeight } = dockStore;
|
||||
const newHeight = height + offsetY;
|
||||
if (height > newHeight && newHeight < minHeight) {
|
||||
setHeight(defaultHeight);
|
||||
close();
|
||||
}
|
||||
else if (isOpen) {
|
||||
setHeight(newHeight);
|
||||
}
|
||||
}
|
||||
|
||||
onKeydown = (evt: React.KeyboardEvent<HTMLElement>) => {
|
||||
const { close, closeTab, selectedTab, fullSize, toggleFillSize } = dockStore;
|
||||
const { close, closeTab, selectedTab } = dockStore;
|
||||
if (!selectedTab) return;
|
||||
const { code, ctrlKey, shiftKey } = evt.nativeEvent;
|
||||
if (shiftKey && code === "Escape") {
|
||||
@@ -71,13 +51,13 @@ export class Dock extends React.Component<Props> {
|
||||
@autobind()
|
||||
renderTab(tab: IDockTab) {
|
||||
if (isTerminalTab(tab)) {
|
||||
return <TerminalTab value={tab}/>
|
||||
return <TerminalTab value={tab} />
|
||||
}
|
||||
if (isCreateResourceTab(tab) || isEditResourceTab(tab)) {
|
||||
return <DockTab value={tab} icon="edit"/>
|
||||
return <DockTab value={tab} icon="edit" />
|
||||
}
|
||||
if (isInstallChartTab(tab) || isUpgradeChartTab(tab)) {
|
||||
return <DockTab value={tab} icon={<Icon svg="install"/>}/>
|
||||
return <DockTab value={tab} icon={<Icon svg="install" />} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,11 +66,11 @@ export class Dock extends React.Component<Props> {
|
||||
if (!isOpen || !tab) return;
|
||||
return (
|
||||
<div className="tab-content" style={{ flexBasis: height }}>
|
||||
{isCreateResourceTab(tab) && <CreateResource tab={tab}/>}
|
||||
{isEditResourceTab(tab) && <EditResource tab={tab}/>}
|
||||
{isInstallChartTab(tab) && <InstallChart tab={tab}/>}
|
||||
{isUpgradeChartTab(tab) && <UpgradeChart tab={tab}/>}
|
||||
{isTerminalTab(tab) && <TerminalWindow tab={tab}/>}
|
||||
{isCreateResourceTab(tab) && <CreateResource tab={tab} />}
|
||||
{isEditResourceTab(tab) && <EditResource tab={tab} />}
|
||||
{isInstallChartTab(tab) && <InstallChart tab={tab} />}
|
||||
{isUpgradeChartTab(tab) && <UpgradeChart tab={tab} />}
|
||||
{isTerminalTab(tab) && <TerminalWindow tab={tab} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -104,11 +84,16 @@ export class Dock extends React.Component<Props> {
|
||||
onKeyDown={this.onKeydown}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Draggable
|
||||
className={cssNames("resizer", { disabled: !hasTabs() })}
|
||||
horizontal={false}
|
||||
onStart={this.onResizeStart}
|
||||
onEnter={this.onResize}
|
||||
<ResizingAnchor
|
||||
disabled={!hasTabs()}
|
||||
getCurrentExtent={() => dockStore.height}
|
||||
minExtent={dockStore.minHeight}
|
||||
maxExtent={dockStore.maxHeight}
|
||||
direction={ResizeDirection.VERTICAL}
|
||||
onStart={dockStore.open}
|
||||
onMinExtentSubceed={dockStore.close}
|
||||
onMinExtentExceed={dockStore.open}
|
||||
onDrag={dockStore.setHeight}
|
||||
/>
|
||||
<div className="tabs-container flex align-center" onDoubleClick={prevDefault(toggle)}>
|
||||
<Tabs
|
||||
@@ -121,11 +106,11 @@ export class Dock extends React.Component<Props> {
|
||||
<div className="dock-menu box grow">
|
||||
<MenuActions usePortal triggerIcon={{ material: "add", className: "new-dock-tab", tooltip: <Trans>New tab</Trans> }} closeOnScroll={false}>
|
||||
<MenuItem className="create-terminal-tab" onClick={() => createTerminalTab()}>
|
||||
<Icon small svg="terminal" size={15}/>
|
||||
<Icon small svg="terminal" size={15} />
|
||||
<Trans>Terminal session</Trans>
|
||||
</MenuItem>
|
||||
<MenuItem className="create-resource-tab" onClick={() => createResourceTab()}>
|
||||
<Icon small material="create"/>
|
||||
<Icon small material="create" />
|
||||
<Trans>Create resource</Trans>
|
||||
</MenuItem>
|
||||
</MenuActions>
|
||||
@@ -133,7 +118,7 @@ export class Dock extends React.Component<Props> {
|
||||
{hasTabs() && (
|
||||
<>
|
||||
<Icon
|
||||
material={fullSize ? "fullscreen_exit": "fullscreen"}
|
||||
material={fullSize ? "fullscreen_exit" : "fullscreen"}
|
||||
tooltip={fullSize ? <Trans>Exit full size mode</Trans> : <Trans>Fit to window</Trans>}
|
||||
onClick={toggleFillSize}
|
||||
/>
|
||||
|
||||
@@ -90,11 +90,6 @@ export class EditResource extends React.Component<Props> {
|
||||
const { kind, getNs, getName } = resource;
|
||||
return (
|
||||
<div className={cssNames("EditResource flex column", this.props.className)}>
|
||||
<EditorPanel
|
||||
tabId={tabId}
|
||||
value={draft}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<InfoPanel
|
||||
tabId={tabId}
|
||||
error={error}
|
||||
@@ -109,6 +104,11 @@ export class EditResource extends React.Component<Props> {
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<EditorPanel
|
||||
tabId={tabId}
|
||||
value={draft}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@include hidden-scrollbar;
|
||||
|
||||
background: $dockInfoBackground;
|
||||
border-top: 1px solid $dockInfoBorderColor;
|
||||
border-bottom: 1px solid $dockInfoBorderColor;
|
||||
padding: $padding $padding * 2;
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -12,37 +12,12 @@
|
||||
|
||||
> .controls {
|
||||
white-space: nowrap;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
flex: 1 1;
|
||||
|
||||
&:not(:empty) + .info {
|
||||
border: 1px solid $borderColor;
|
||||
border-top: 0;
|
||||
border-bottom: 0;
|
||||
min-height: 25px;
|
||||
padding-left: $padding;
|
||||
padding-right: $padding;
|
||||
}
|
||||
}
|
||||
|
||||
> .info {
|
||||
@include hidden-scrollbar;
|
||||
|
||||
min-width: 40px; // min-space for icon
|
||||
flex: 1 1;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
> div {
|
||||
padding-right: $padding;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.Icon {
|
||||
margin: 0;
|
||||
margin-right: $padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,35 +38,29 @@ export class InfoPanel extends Component<Props> {
|
||||
showNotifications: true,
|
||||
}
|
||||
|
||||
@observable.ref result: ReactNode;
|
||||
@observable error = "";
|
||||
@observable waiting = false;
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.props.tabId, () => {
|
||||
this.result = ""
|
||||
this.error = ""
|
||||
this.waiting = false
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
@computed get errorInfo() {
|
||||
return this.error || this.props.error;
|
||||
return this.props.error;
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
const { showNotifications } = this.props;
|
||||
this.result = "";
|
||||
this.error = "";
|
||||
this.waiting = true;
|
||||
try {
|
||||
this.result = await this.props.submit()
|
||||
if (showNotifications) Notifications.ok(this.result);
|
||||
const result = await this.props.submit();
|
||||
if (showNotifications) Notifications.ok(result);
|
||||
} catch (error) {
|
||||
this.error = error.toString();
|
||||
if (showNotifications) Notifications.error(this.error);
|
||||
if (showNotifications) Notifications.error(error.toString());
|
||||
} finally {
|
||||
this.waiting = false
|
||||
}
|
||||
@@ -81,27 +75,15 @@ export class InfoPanel extends Component<Props> {
|
||||
dockStore.closeTab(this.props.tabId);
|
||||
}
|
||||
|
||||
renderInfo() {
|
||||
if (!this.props.showInlineInfo) {
|
||||
renderErrorIcon() {
|
||||
if (!this.props.showInlineInfo || !this.errorInfo) {
|
||||
return;
|
||||
}
|
||||
const { result, errorInfo } = this;
|
||||
return (
|
||||
<>
|
||||
{result && (
|
||||
<div className="success flex align-center">
|
||||
<Icon material="done" />
|
||||
<span>{result}</span>
|
||||
</div>
|
||||
)}
|
||||
{errorInfo && (
|
||||
<div className="error flex align-center">
|
||||
<Icon material="error_outline" />
|
||||
<span>{errorInfo}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
<div className="error">
|
||||
<Icon material="error_outline" tooltip={this.errorInfo}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -114,11 +96,13 @@ export class InfoPanel extends Component<Props> {
|
||||
{controls}
|
||||
</div>
|
||||
<div className="info flex gaps align-center">
|
||||
{waiting ? <><Spinner /> {submittingMessage}</> : this.renderInfo()}
|
||||
{waiting ? <><Spinner /> {submittingMessage}</> : this.renderErrorIcon()}
|
||||
</div>
|
||||
<Button plain label={<Trans>Cancel</Trans>} onClick={close} />
|
||||
<Button
|
||||
primary active
|
||||
active
|
||||
outlined={showSubmitClose}
|
||||
primary={!showSubmitClose}// one button always should be primary (blue)
|
||||
label={submitLabel}
|
||||
onClick={submit}
|
||||
disabled={isDisabled}
|
||||
|
||||
@@ -174,11 +174,6 @@ export class InstallChart extends Component<Props> {
|
||||
|
||||
return (
|
||||
<div className="InstallChart flex column">
|
||||
<EditorPanel
|
||||
tabId={tabId}
|
||||
value={values}
|
||||
onChange={this.onValuesChange}
|
||||
/>
|
||||
<InfoPanel
|
||||
tabId={tabId}
|
||||
controls={panelControls}
|
||||
@@ -188,6 +183,11 @@ export class InstallChart extends Component<Props> {
|
||||
submittingMessage={_i18n._(t`Installing...`)}
|
||||
showSubmitClose={false}
|
||||
/>
|
||||
<EditorPanel
|
||||
tabId={tabId}
|
||||
value={values}
|
||||
onChange={this.onValuesChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,6 +121,8 @@ export class Terminal {
|
||||
}
|
||||
|
||||
fit = () => {
|
||||
// Since this function is debounced we need to read this value as late as possible
|
||||
if (!this.isActive) return;
|
||||
this.fitAddon.fit();
|
||||
const { cols, rows } = this.xterm;
|
||||
this.api.sendTerminalSize(cols, rows);
|
||||
@@ -150,7 +152,6 @@ export class Terminal {
|
||||
}
|
||||
|
||||
onResize = () => {
|
||||
if (!this.isActive) return;
|
||||
this.fitLazy();
|
||||
this.focus();
|
||||
}
|
||||
@@ -176,8 +177,8 @@ export class Terminal {
|
||||
if (this.xterm.hasSelection()) return false;
|
||||
break;
|
||||
|
||||
// Ctrl+W: prevent unexpected terminal tab closing, e.g. editing file in vim
|
||||
// https://github.com/kontena/lens-app/issues/156#issuecomment-534906480
|
||||
// Ctrl+W: prevent unexpected terminal tab closing, e.g. editing file in vim
|
||||
// https://github.com/kontena/lens-app/issues/156#issuecomment-534906480
|
||||
case "KeyW":
|
||||
evt.preventDefault();
|
||||
break;
|
||||
|
||||
@@ -106,18 +106,13 @@ export class UpgradeChart extends React.Component<Props> {
|
||||
themeName="outlined"
|
||||
value={version}
|
||||
options={versions}
|
||||
formatOptionLabel={this.formatVersionLabel}
|
||||
formatOptionLabel={this.formatVersionLabel}
|
||||
onChange={({ value }: SelectOption) => this.version = value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className={cssNames("UpgradeChart flex column", className)}>
|
||||
<EditorPanel
|
||||
tabId={tabId}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<InfoPanel
|
||||
tabId={tabId}
|
||||
error={error}
|
||||
@@ -126,6 +121,11 @@ export class UpgradeChart extends React.Component<Props> {
|
||||
submittingMessage={_i18n._(t`Updating..`)}
|
||||
controls={controlsAndInfo}
|
||||
/>
|
||||
<EditorPanel
|
||||
tabId={tabId}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
body.dragging {
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import "./draggable.scss";
|
||||
import React from "react";
|
||||
import { cssNames, IClassName, noop } from "../../utils";
|
||||
import throttle from "lodash/throttle";
|
||||
|
||||
export interface DraggableEventHandler {
|
||||
(state: DraggableState): void;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
className?: IClassName;
|
||||
vertical?: boolean;
|
||||
horizontal?: boolean;
|
||||
onStart?: DraggableEventHandler;
|
||||
onEnter?: DraggableEventHandler;
|
||||
onEnd?: DraggableEventHandler;
|
||||
}
|
||||
|
||||
export interface DraggableState {
|
||||
inited?: boolean;
|
||||
changed?: boolean;
|
||||
initX?: number;
|
||||
initY?: number;
|
||||
pageX?: number;
|
||||
pageY?: number;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
}
|
||||
|
||||
const initState: DraggableState = {
|
||||
inited: false,
|
||||
changed: false,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
|
||||
export class Draggable extends React.PureComponent<Props, DraggableState> {
|
||||
public state = initState;
|
||||
|
||||
static IS_DRAGGING = "dragging"
|
||||
|
||||
static defaultProps: Props = {
|
||||
vertical: true,
|
||||
horizontal: true,
|
||||
onStart: noop,
|
||||
onEnter: noop,
|
||||
onEnd: noop,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
document.addEventListener("mousemove", this.onDrag);
|
||||
document.addEventListener("mouseup", this.onDragEnd);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("mousemove", this.onDrag);
|
||||
document.removeEventListener("mouseup", this.onDragEnd);
|
||||
}
|
||||
|
||||
onDragInit = (evt: React.MouseEvent<any>) => {
|
||||
document.body.classList.add(Draggable.IS_DRAGGING);
|
||||
const { pageX, pageY } = evt;
|
||||
this.setState({
|
||||
inited: true,
|
||||
initX: pageX,
|
||||
initY: pageY,
|
||||
pageX: pageX,
|
||||
pageY: pageY,
|
||||
})
|
||||
}
|
||||
|
||||
onDrag = throttle((evt: MouseEvent) => {
|
||||
const { vertical, horizontal, onEnter, onStart } = this.props;
|
||||
const { inited, pageX, pageY } = this.state;
|
||||
const offsetX = pageX - evt.pageX;
|
||||
const offsetY = pageY - evt.pageY;
|
||||
let changed = false;
|
||||
if (horizontal && offsetX !== 0) changed = true;
|
||||
if (vertical && offsetY !== 0) changed = true;
|
||||
if (inited && changed) {
|
||||
const start = !this.state.changed;
|
||||
const state = Object.assign({}, this.state, {
|
||||
changed: true,
|
||||
pageX: evt.pageX,
|
||||
pageY: evt.pageY,
|
||||
offsetX: offsetX,
|
||||
offsetY: offsetY,
|
||||
});
|
||||
if (start) onStart(state);
|
||||
this.setState(state, () => onEnter(state));
|
||||
}
|
||||
}, 100)
|
||||
|
||||
onDragEnd = (evt: MouseEvent) => {
|
||||
const { pageX, pageY } = evt;
|
||||
const { inited, changed, initX, initY } = this.state;
|
||||
if (inited) {
|
||||
document.body.classList.remove(Draggable.IS_DRAGGING);
|
||||
this.setState(initState, () => {
|
||||
if (!changed) return;
|
||||
const state = Object.assign({}, this.state, {
|
||||
offsetX: initX - pageX,
|
||||
offsetY: initY - pageY,
|
||||
});
|
||||
this.props.onEnd(state);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, children } = this.props;
|
||||
return (
|
||||
<div className={cssNames("Draggable", className)} onMouseDown={this.onDragInit}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './draggable'
|
||||
@@ -48,6 +48,11 @@
|
||||
float: left;
|
||||
margin: $spacing;
|
||||
|
||||
&:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { isEmail, systemName } from "./input.validators";
|
||||
import { isEmail, systemName } from "../input_validators";
|
||||
|
||||
describe("input validation tests", () => {
|
||||
describe("isEmail tests", () => {
|
||||
@@ -3,7 +3,7 @@ import "./input.scss";
|
||||
import React, { DOMAttributes, InputHTMLAttributes, TextareaHTMLAttributes } from "react";
|
||||
import { autobind, cssNames, debouncePromise } from "../../utils";
|
||||
import { Icon } from "../icon";
|
||||
import { conditionalValidators, Validator } from "./input.validators";
|
||||
import { conditionalValidators, Validator } from "./input_validators";
|
||||
import isString from "lodash/isString"
|
||||
import isFunction from "lodash/isFunction"
|
||||
import isBoolean from "lodash/isBoolean"
|
||||
@@ -288,9 +288,9 @@ export class Input extends React.Component<InputProps, State> {
|
||||
return (
|
||||
<div className={className}>
|
||||
<label className="input-area flex gaps align-center">
|
||||
{isString(iconLeft) ? <Icon material={iconLeft}/> : iconLeft}
|
||||
{multiLine ? <textarea {...inputProps as any}/> : <input {...inputProps as any}/>}
|
||||
{isString(iconRight) ? <Icon material={iconRight}/> : iconRight}
|
||||
{isString(iconLeft) ? <Icon material={iconLeft} /> : iconLeft}
|
||||
{multiLine ? <textarea {...inputProps as any} /> : <input {...inputProps as any} />}
|
||||
{isString(iconRight) ? <Icon material={iconRight} /> : iconRight}
|
||||
</label>
|
||||
<div className="input-info flex gaps">
|
||||
{!valid && dirty && (
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1 @@
|
||||
export * from './resizing-anchor'
|
||||
@@ -0,0 +1,46 @@
|
||||
body.resizing {
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.ResizingAnchor {
|
||||
$dimension: 12px;
|
||||
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
|
||||
&.disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
left: 0;
|
||||
right: 0;
|
||||
cursor: row-resize;
|
||||
height: $dimension;
|
||||
|
||||
&.leading {
|
||||
top: -$dimension / 2;
|
||||
}
|
||||
|
||||
&.trailing {
|
||||
bottom: -$dimension / 2;
|
||||
}
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
cursor: col-resize;
|
||||
width: $dimension;
|
||||
|
||||
&.leading {
|
||||
left: -$dimension / 2;
|
||||
}
|
||||
|
||||
&.trailing {
|
||||
right: -$dimension / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
import "./resizing-anchor.scss";
|
||||
import React from "react";
|
||||
import { action, observable } from "mobx";
|
||||
import _ from "lodash"
|
||||
import { findDOMNode } from "react-dom";
|
||||
import { cssNames, noop } from "../../utils";
|
||||
|
||||
export enum ResizeDirection {
|
||||
HORIZONTAL = "horizontal",
|
||||
VERTICAL = "vertical",
|
||||
}
|
||||
|
||||
/**
|
||||
* ResizeSide is for customizing where the area should be rendered.
|
||||
* That location is determined in conjunction with the `ResizeDirection` using the following table:
|
||||
*
|
||||
* +----------+------------+----------+
|
||||
* | | HORIZONTAL | VERTICAL |
|
||||
* +----------+------------+----------+
|
||||
* | LEADING | left | top |
|
||||
* +----------+------------+----------+
|
||||
* | TRAILING | right | bottom |
|
||||
* +----------+------------+----------+
|
||||
*/
|
||||
export enum ResizeSide {
|
||||
LEADING = "leading",
|
||||
TRAILING = "trailing",
|
||||
}
|
||||
|
||||
/**
|
||||
* ResizeGrowthDirection determines how the anchor interprets the drag.
|
||||
*
|
||||
* Because the origin of the screen is top left a drag from bottom to top
|
||||
* results in a negative directional delta. However, if the component being
|
||||
* dragged grows in the opposite direction, this needs to be compensated for.
|
||||
*/
|
||||
export enum ResizeGrowthDirection {
|
||||
TOP_TO_BOTTOM = 1,
|
||||
BOTTOM_TO_TOP = -1,
|
||||
LEFT_TO_RIGHT = 1,
|
||||
RIGHT_TO_LEFT = -1,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
direction: ResizeDirection;
|
||||
|
||||
/**
|
||||
* getCurrentExtent should return the current prominent dimension in the
|
||||
* given resizing direction. Width for HORIZONTAL and height for VERTICAL
|
||||
*/
|
||||
getCurrentExtent: () => number;
|
||||
|
||||
disabled?: boolean;
|
||||
placement?: ResizeSide;
|
||||
growthDirection?: ResizeGrowthDirection;
|
||||
|
||||
// Ability to restrict which mouse buttons are allowed to resize this component
|
||||
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
|
||||
onlyButtons?: number;
|
||||
|
||||
// onStart is called when the ResizeAnchor is first clicked (mouse down)
|
||||
onStart?: () => void;
|
||||
|
||||
// onEnd is called when the ResizeAnchor is released (mouse up)
|
||||
onEnd?: () => void;
|
||||
|
||||
/**
|
||||
* onDrag is called whenever there is a mousemove event. All calls will be
|
||||
* bounded by matching `onStart` and `onEnd` calls.
|
||||
*/
|
||||
onDrag?: (newExtent: number) => void;
|
||||
|
||||
// onDoubleClick is called when the the ResizeAnchor is double clicked
|
||||
onDoubleClick?: () => void;
|
||||
|
||||
/**
|
||||
* The following two extents represent the max and min values set to `onDrag`
|
||||
*/
|
||||
maxExtent?: number;
|
||||
minExtent?: number;
|
||||
|
||||
/**
|
||||
* The following events are triggerred with respect to the above values.
|
||||
* - The "__Exceed" call will be made when the unbounded extent goes from
|
||||
* < the above to >= the above
|
||||
* - The "__Subceed" call is similar but is triggered when the unbounded
|
||||
* extent goes from >= the above to < the above.
|
||||
*/
|
||||
onMaxExtentExceed?: () => void;
|
||||
onMaxExtentSubceed?: () => void;
|
||||
onMinExtentSubceed?: () => void;
|
||||
onMinExtentExceed?: () => void;
|
||||
}
|
||||
|
||||
interface Position {
|
||||
readonly pageX: number;
|
||||
readonly pageY: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the direction delta, but ignore drags leading up to a moved item
|
||||
* 1. `->|` => return `false`
|
||||
* 2. `<-|` => return `directed length (M, P2)` (negative)
|
||||
* 3. `-|>` => return `directed length (M, P2)` (positive)
|
||||
* 4. `<|-` => return `directed length (M, P2)` (negative)
|
||||
* 5. `|->` => return `directed length (M, P2)` (positive)
|
||||
* 6. `|<-` => return `false`
|
||||
* @param P1 the starting position on the number line
|
||||
* @param P2 the ending position on the number line
|
||||
* @param M a third point that determines if the delta is meaningful
|
||||
* @returns the directional difference between including appropriate sign.
|
||||
*/
|
||||
function directionDelta(P1: number, P2: number, M: number): number | false {
|
||||
const delta = Math.abs(M - P2)
|
||||
|
||||
if (P1 < M) {
|
||||
if (P2 >= M) {
|
||||
// case 3
|
||||
return delta
|
||||
}
|
||||
|
||||
if (P2 < P1) {
|
||||
// case 2
|
||||
return -delta
|
||||
}
|
||||
|
||||
// case 1
|
||||
return false
|
||||
}
|
||||
|
||||
if (P2 < M) {
|
||||
// case 4
|
||||
return -delta
|
||||
}
|
||||
|
||||
if (P1 < P2) {
|
||||
// case 5
|
||||
return delta
|
||||
}
|
||||
|
||||
// case 6
|
||||
return false
|
||||
}
|
||||
|
||||
export class ResizingAnchor extends React.PureComponent<Props> {
|
||||
@observable lastMouseEvent?: MouseEvent
|
||||
@observable.ref ref?: React.RefObject<HTMLDivElement>;
|
||||
|
||||
static defaultProps = {
|
||||
onStart: noop,
|
||||
onDrag: noop,
|
||||
onEnd: noop,
|
||||
onMaxExtentExceed: noop,
|
||||
onMinExtentExceed: noop,
|
||||
onMinExtentSubceed: noop,
|
||||
onMaxExtentSubceed: noop,
|
||||
onDoubleClick: noop,
|
||||
disabled: false,
|
||||
growthDirection: ResizeGrowthDirection.BOTTOM_TO_TOP,
|
||||
maxExtent: Number.POSITIVE_INFINITY,
|
||||
minExtent: 0,
|
||||
placement: ResizeSide.LEADING,
|
||||
}
|
||||
static IS_RESIZING = "resizing"
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
if (props.maxExtent < props.minExtent) {
|
||||
throw new Error("maxExtent must be >= minExtent")
|
||||
}
|
||||
|
||||
this.ref = React.createRef<HTMLDivElement>()
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("mousemove", this.onDrag)
|
||||
document.removeEventListener("mouseup", this.onDragEnd)
|
||||
}
|
||||
|
||||
@action
|
||||
onDragInit = (event: React.MouseEvent) => {
|
||||
const { onStart, onlyButtons } = this.props
|
||||
|
||||
if (typeof onlyButtons === "number" && onlyButtons !== event.buttons) {
|
||||
return
|
||||
}
|
||||
|
||||
document.addEventListener("mousemove", this.onDrag)
|
||||
document.addEventListener("mouseup", this.onDragEnd)
|
||||
document.body.classList.add(ResizingAnchor.IS_RESIZING)
|
||||
|
||||
this.lastMouseEvent = undefined
|
||||
onStart()
|
||||
}
|
||||
|
||||
calculateDelta(from: Position, to: Position): number | false {
|
||||
const node = this.ref.current
|
||||
if (!node) {
|
||||
return false
|
||||
}
|
||||
|
||||
const boundingBox = node.getBoundingClientRect()
|
||||
|
||||
if (this.props.direction === ResizeDirection.HORIZONTAL) {
|
||||
const barX = Math.round(boundingBox.x + (boundingBox.width / 2))
|
||||
return directionDelta(from.pageX, to.pageX, barX)
|
||||
} else { // direction === ResizeDirection.VERTICAL
|
||||
const barY = Math.round(boundingBox.y + (boundingBox.height / 2))
|
||||
return directionDelta(from.pageY, to.pageY, barY)
|
||||
}
|
||||
}
|
||||
|
||||
onDrag = _.throttle((event: MouseEvent) => {
|
||||
/**
|
||||
* Some notes to help understand the following:
|
||||
* - A browser's origin point is in the top left of the screen
|
||||
* - X increases going from left to right
|
||||
* - Y increases going from top to bottom
|
||||
* - Since the resize bar should always be a rectangle, use its centre
|
||||
* line (in the resizing direction) as the line for determining if
|
||||
* the bar has "jumped around"
|
||||
*
|
||||
* Desire:
|
||||
* - Always ignore movement in the non-resizing direction
|
||||
* - Figure out how much the user has "dragged" the resize bar
|
||||
* - If the resize bar has jumped around, compensate by ignoring movement
|
||||
* in the resizing direction if it is moving "towards" the resize bar's
|
||||
* new location.
|
||||
*/
|
||||
|
||||
if (!this.lastMouseEvent) {
|
||||
this.lastMouseEvent = event
|
||||
return
|
||||
}
|
||||
|
||||
const { maxExtent, minExtent, getCurrentExtent, growthDirection } = this.props
|
||||
const { onDrag, onMaxExtentExceed, onMinExtentSubceed, onMaxExtentSubceed, onMinExtentExceed } = this.props
|
||||
const delta = this.calculateDelta(this.lastMouseEvent, event)
|
||||
|
||||
// always update the last mouse event
|
||||
this.lastMouseEvent = event
|
||||
|
||||
if (delta === false) {
|
||||
return
|
||||
}
|
||||
|
||||
const previousExtent = getCurrentExtent()
|
||||
const unboundedExtent = previousExtent + (delta * growthDirection)
|
||||
const boundedExtent = Math.round(Math.max(minExtent, Math.min(maxExtent, unboundedExtent)))
|
||||
onDrag(boundedExtent)
|
||||
|
||||
if (previousExtent <= minExtent && minExtent <= unboundedExtent) {
|
||||
onMinExtentExceed()
|
||||
} else if (previousExtent >= minExtent && minExtent >= unboundedExtent) {
|
||||
onMinExtentSubceed()
|
||||
}
|
||||
if (previousExtent <= maxExtent && maxExtent <= unboundedExtent) {
|
||||
onMaxExtentExceed()
|
||||
} else if (previousExtent >= maxExtent && maxExtent >= unboundedExtent) {
|
||||
onMaxExtentSubceed()
|
||||
}
|
||||
}, 100)
|
||||
|
||||
@action
|
||||
onDragEnd = (_event: MouseEvent) => {
|
||||
this.props.onEnd()
|
||||
document.removeEventListener("mousemove", this.onDrag)
|
||||
document.removeEventListener("mouseup", this.onDragEnd)
|
||||
document.body.classList.remove(ResizingAnchor.IS_RESIZING)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { disabled, direction, placement, onDoubleClick } = this.props
|
||||
return <div
|
||||
ref={this.ref}
|
||||
className={cssNames("ResizingAnchor", direction, placement, { disabled })}
|
||||
onMouseDown={this.onDragInit}
|
||||
onDoubleClick={onDoubleClick}
|
||||
/>
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export function withTooltip<T extends React.ComponentType<any>>(Target: T): T {
|
||||
const tooltipProps: TooltipProps = {
|
||||
targetId: tooltipId,
|
||||
tooltipOnParentHover: tooltipOverrideDisabled,
|
||||
formatters: { narrow: true },
|
||||
...(isReactNode(tooltip) ? { children: tooltip } : tooltip),
|
||||
};
|
||||
targetProps.id = tooltipId;
|
||||
|
||||
Reference in new issue
Block a user