feat(frontend): Script page action row (#626)

* feat(frontend): Update button component styles

* feat(frontend): Use button component

* fix(frontend): Re-export button types

* feat(frontend): Use action row in script page

* fix(frontend): Center the action row

* fix(frontend): Revert to previous event forwarding

* feature(frontend): Add action row to flow page

* fix(frontend): Restore button red color
This commit is contained in:
Ádám Kovács
2022-09-26 17:18:17 +02:00
committed by GitHub
parent 8e40c79557
commit dc2bbca269
6 changed files with 328 additions and 250 deletions
@@ -0,0 +1,27 @@
<script lang="ts">
export let applyPageWidth = false
let classes = 'max-w-6xl mx-auto px-4 sm:px-6 md:px-8'
</script>
<div class={'bg-white py-3 ' + $$props.class}>
<div
class={'w-full flex flex-wrap justify-between items-center gap-4 ' +
(applyPageWidth ? classes : '')}
>
{#if $$slots.left}
<div class="flex flex-wrap items-center gap-2">
<slot name="left" />
</div>
{/if}
{#if $$slots.middle}
<div class="flex flex-wrap items-center gap-2">
<slot name="middle" />
</div>
{/if}
{#if $$slots.right}
<div class="flex flex-wrap items-center gap-2">
<slot name="right" />
</div>
{/if}
</div>
</div>
@@ -1,81 +1,108 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import Icon from 'svelte-awesome'
import type { Button } from './model'
export let size: 'xs' | 'sm' | 'md' | 'lg' | 'xl' = 'md'
export let color: 'blue' | 'dark' | 'light' | 'red' = 'blue'
export let variant: 'contained' | 'border' = 'contained'
export let size: Button.Size = 'md'
export let color: Button.Color = 'blue'
export let variant: Button.Variant = 'contained'
export let btnClasses: string = ''
export let disabled: boolean = false
export let href: string | undefined = undefined
export let target: '_self' | '_blank' = '_self'
export let target: Button.Target = '_self'
export let startIcon: { icon: any; classes?: string } | undefined = undefined
export let endIcon: { icon: any; classes?: string } | undefined = undefined
function getColorClasses() {
switch (color) {
case 'blue':
return 'bg-blue-500 hover:bg-blue-700 focus:ring-blue-300 text-white '
case 'red':
return 'bg-red-500 hover:bg-red-700 focus:ring-red-300 text-white '
case 'light':
return 'text-gray-800 bg-white hover:bg-gray-100 focus:ring-gray-300'
default:
return ''
// Order of classes: border, border modifier, bg, bg modifier, text, text modifier, everything else
const colorVariants: Record<Button.Color, Record<Button.Variant, string>> = {
blue: {
border:
'border-blue-500 hover:border-blue-700 bg-white hover:bg-blue-100 text-blue-500 hover:text-blue-700 focus:ring-blue-300',
contained: 'bg-blue-500 hover:bg-blue-700 text-white focus:ring-blue-300'
},
red: {
border:
'border-red-500 hover:border-red-700 bg-white hover:bg-red-100 text-red-500 hover:text-red-700 focus:ring-red-300',
contained: 'bg-red-500 hover:bg-red-700 text-white focus:ring-red-300'
},
dark: {
border:
'border-gray-800 hover:border-gray-900 bg-white hover:bg-gray-200 text-gray-800 hover:text-gray-900 focus:ring-gray-300',
contained: 'bg-gray-700 hover:bg-gray-900 text-white focus:ring-gray-300'
},
light: {
border:
'border-gray-700 hover:border-gray-800 bg-white hover:bg-gray-100 text-gray-700 hover:text-gray-800 focus:ring-gray-300',
contained: 'bg-white hover:bg-gray-100 text-gray-700 focus:ring-gray-300'
}
}
function getSizeClasses() {
switch (size) {
case 'xs':
return 'text-xs'
case 'sm':
return 'text-sm'
case 'md':
return 'text-md'
case 'lg':
return 'text-lg'
case 'xl':
return 'text-xl'
default:
return ''
}
const sizeClass: Record<Button.Size, string> = {
xs: 'text-xs px-2.5 py-1',
sm: 'text-sm px-2.5 py-1',
md: 'text-md px-3 py-1',
lg: 'text-lg px-4 py-2',
xl: 'text-xl px-5 py-2'
}
const iconScale: Record<Button.Size, number> = {
xs: 0.7,
sm: 0.8,
md: 1,
lg: 1.1,
xl: 1.2
}
const buttonProps = {
class: classNames(
getColorClasses(),
getSizeClasses(),
'focus:ring-4 font-medium',
'px-4 py-2',
'rounded-md',
'flex justify-center items-center',
colorVariants[color][variant],
variant === 'border' ? 'border' : '',
btnClasses
sizeClass[size],
'focus:ring-4 font-medium',
'rounded-md',
'flex justify-center items-center text-center',
btnClasses,
disabled ? 'pointer-events-none cursor-default filter grayscale' : ''
),
disabled
}
</script>
{#if href}
<a {href} {target} on:click|stopPropagation {...buttonProps}>
<a {href} {target} tabindex={disabled ? -1 : 0} on:click|stopPropagation {...buttonProps}>
{#if startIcon}
<Icon data={startIcon.icon} class={classNames('mr-2', startIcon.classes)} />
<Icon
data={startIcon.icon}
class={classNames('mr-2', startIcon.classes)}
scale={iconScale[size]}
/>
{/if}
<slot />
{#if endIcon}
<Icon data={endIcon.icon} class={classNames('ml-2', endIcon.classes)} />
<Icon
data={endIcon.icon}
class={classNames('ml-2', endIcon.classes)}
scale={iconScale[size]}
/>
{/if}
</a>
{:else}
<button type="button" on:click|stopPropagation {...buttonProps}>
{#if startIcon}
<Icon data={startIcon.icon} class={classNames('mr-2', startIcon.classes)} />
<Icon
data={startIcon.icon}
class={classNames('mr-2', startIcon.classes)}
scale={iconScale[size]}
/>
{/if}
<slot />
{#if endIcon}
<Icon data={endIcon.icon} class={classNames('ml-2', endIcon.classes)} />
<Icon
data={endIcon.icon}
class={classNames('ml-2', endIcon.classes)}
scale={iconScale[size]}
/>
{/if}
</button>
{/if}
@@ -0,0 +1,6 @@
export namespace Button {
export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
export type Color = 'blue' | 'red' | 'dark' | 'light'
export type Variant = 'contained' | 'border'
export type Target = '_self' | '_blank'
}
@@ -0,0 +1,15 @@
export { default as ActionRow } from './actionRow/ActionRow.svelte'
export { default as Badge } from './badge/Badge.svelte'
export { default as Button } from './button/Button.svelte'
export { default as Drawer } from './drawer/Drawer.svelte'
export { default as DrawerContent } from './drawer/DrawerContent.svelte'
export { default as Menu } from './menu/Menu.svelte'
export { default as MenuItem } from './menu/MenuItem.svelte'
export { default as Tab } from './tabs/Tab.svelte'
export { default as TabContent } from './tabs/TabContent.svelte'
export { default as Tabs } from './tabs/Tabs.svelte'
export { default as ToggleButton } from './toggleButton/ToggleButton.svelte'
export { default as ToggleButtonGroup } from './toggleButton/ToggleButtonGroup.svelte'
export * from './badge/model'
export * from './button/model'
+95 -97
View File
@@ -38,6 +38,7 @@
import CenteredPage from '$lib/components/CenteredPage.svelte'
import FlowViewer from '$lib/components/FlowViewer.svelte'
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
import { Button, ActionRow } from '$lib/components/common'
let flow: Flow | undefined
let schedule: Schedule | undefined
@@ -45,6 +46,7 @@
let path = $page.params.path
let shareModal: ShareModal
let scrollY: number
$: {
if ($workspaceStore && $userStore) {
@@ -88,105 +90,101 @@
}
</script>
<svelte:window bind:scrollY />
{#if flow}
<ActionRow applyPageWidth class={'sticky top-0 ' + (scrollY >= 30 ? 'border-b' : '')}>
<svelte:fragment slot="left">
<Button
href="/flows/run/{path}"
variant="contained"
color="blue"
size="md"
startIcon={{ icon: faPlay }}
>
Run
</Button>
<Button
href="/flows/edit/{path}"
variant="contained"
color="blue"
size="md"
startIcon={{ icon: faEdit }}
disabled={!can_write}
>
Edit
</Button>
<Button
href="/flows/add?template={flow.path}"
variant="contained"
color="blue"
size="md"
startIcon={{ icon: faCodeFork }}
>
Use as template/Fork
</Button>
</svelte:fragment>
<svelte:fragment slot="right">
<Button
href="/runs/{flow.path}"
variant="border"
color="blue"
size="md"
startIcon={{ icon: faList }}
>
View runs
</Button>
<Button
target="_blank"
href={flowToHubUrl(flow).toString()}
variant="border"
color="blue"
size="md"
startIcon={{ icon: faGlobe }}
>
Publish to Hub
</Button>
<Dropdown
dropdownItems={[
{
displayName: 'Use as template',
icon: faEdit,
href: `/flows/add?template=${flow.path}`
},
{
displayName: 'Share',
icon: faShare,
action: () => {
shareModal.openModal()
},
disabled: !can_write
},
{
displayName: 'Schedule',
icon: faCalendar,
href: `/schedule/add?path=${flow.path}&isFlow=true`
},
{
displayName: 'Archive',
icon: faArchive,
type: 'delete',
action: () => {
flow?.path && archiveFlow()
},
disabled: flow.archived || !can_write
}
]}
/>
</svelte:fragment>
</ActionRow>
{/if}
<CenteredPage>
<div class="flex flex-row justify-between">
<h1>
<a href="/flows/get/{path}">{flow?.path ?? 'Loading...'}</a>
<h1>
<a href="/flows/get/{path}">{flow?.path ?? 'Loading...'}</a>
<SharedBadge canWrite={can_write} extraPerms={flow?.extra_perms ?? {}} />
</h1>
{#if flow}
<div class="flex flex-row-reverse px-6">
<Dropdown
dropdownItems={[
{
displayName: 'Use as template',
icon: faEdit,
href: `/flows/add?template=${flow.path}`
},
{
displayName: 'Share',
icon: faShare,
action: () => {
shareModal.openModal()
},
disabled: !can_write
},
{
displayName: 'Schedule',
icon: faCalendar,
href: `/schedule/add?path=${flow.path}&isFlow=true`
},
{
displayName: 'Archive',
icon: faArchive,
type: 'delete',
action: () => {
flow?.path && archiveFlow()
},
disabled: flow.archived || !can_write
}
]}
/>
<div class="px-1">
<a
target="_blank"
class="inline-flex items-center default-button bg-transparent hover:bg-blue-500 text-blue-700 font-normal hover:text-white py-0 px-1 border-blue-500 hover:border-transparent rounded"
href={flowToHubUrl(flow).toString()}
>
<div class="inline-flex items-center justify-center">
<Icon class="text-blue-500" data={faGlobe} scale={0.5} />
<span class="pl-1">Publish to Hub</span>
</div>
</a>
</div>
<div class="px-1">
<a
class="inline-flex items-center default-button bg-transparent hover:bg-blue-500 text-blue-700 font-normal hover:text-white py-0 px-1 border-blue-500 hover:border-transparent rounded"
href="/runs/{flow.path}"
>
<div class="inline-flex items-center justify-center">
<Icon class="text-blue-500" data={faList} scale={0.5} />
<span class="pl-1">View runs</span>
</div>
</a>
</div>
<div class="px-1">
<a
class="inline-flex items-center default-button bg-transparent hover:bg-blue-500 text-blue-700 font-normal hover:text-white py-0 px-1 border-blue-500 hover:border-transparent rounded"
href="/flows/edit/{path}"
class:disabled={!can_write}
>
<div class="inline-flex items-center justify-center">
<Icon class="text-blue-500" data={faEdit} scale={0.5} />
<span class="pl-1">Edit</span>
</div>
</a>
<a
class="inline-flex items-center default-button bg-transparent hover:bg-blue-500 text-blue-700 font-normal hover:text-white py-0 px-1 border-blue-500 hover:border-transparent rounded"
href="/flows/add?template={flow.path}"
>
<div class="inline-flex items-center justify-center">
<Icon class="text-blue-500" data={faCodeFork} scale={0.5} />
<span class="pl-1">Use as template/Fork</span>
</div>
</a>
</div>
<div class="px-1">
<a
class="inline-flex items-center default-button bg-transparent hover:bg-blue-500 text-blue-700 font-normal hover:text-white py-0 px-1 border-blue-500 hover:border-transparent rounded"
href="/flows/run/{path}"
>
<div class="inline-flex items-center justify-center">
<Icon class="text-blue-500" data={faPlay} scale={0.5} />
<span class="pl-1">Run</span>
</div>
</a>
</div>
</div>
{/if}
</div>
<SharedBadge canWrite={can_write} extraPerms={flow?.extra_perms ?? {}} />
</h1>
<ShareModal bind:this={shareModal} kind="flow" path={flow?.path ?? ''} />
+117 -112
View File
@@ -18,7 +18,6 @@
scriptToHubUrl,
copyToClipboard
} from '$lib/utils'
import Icon from 'svelte-awesome'
import {
faPlay,
faEdit,
@@ -43,16 +42,14 @@
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { onDestroy } from 'svelte'
import HighlightCode from '$lib/components/HighlightCode.svelte'
import Badge from '$lib/components/common/badge/Badge.svelte'
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
import Tab from '$lib/components/common/tabs/Tab.svelte'
import TabContent from '$lib/components/common/tabs/TabContent.svelte'
import { Badge, Tabs, Tab, TabContent, Button, ActionRow } from '$lib/components/common'
let script: Script | undefined
let topHash: string | undefined
let can_write = false
let deploymentInProgress = false
let intervalId: NodeJS.Timer
let scrollY: number
let shareModal: ShareModal
@@ -131,6 +128,111 @@
})
</script>
<svelte:window bind:scrollY />
{#if script}
<ActionRow applyPageWidth class={'sticky top-0 ' + (scrollY >= 30 ? 'border-b' : '')}>
<svelte:fragment slot="left">
<Button
href={`/scripts/run/${script.hash}`}
variant="contained"
color="blue"
size="md"
startIcon={{ icon: faPlay }}
>
Run
</Button>
<Button
href={`/scripts/edit/${script.hash}?step=2`}
variant="contained"
color="blue"
size="md"
startIcon={{ icon: faEdit }}
disabled={!can_write}
>
Edit
</Button>
{#if !topHash}
<Button
href={`/scripts/add?template=${script.path}`}
variant="contained"
color="blue"
size="md"
startIcon={{ icon: faCodeFork }}
>
Use as template/Fork
</Button>
{/if}
</svelte:fragment>
<svelte:fragment slot="right">
<Button
href={`/runs/${script.path}`}
variant="border"
color="blue"
size="md"
startIcon={{ icon: faList }}
>
View runs
</Button>
<Button
target="_blank"
href={scriptToHubUrl(
script.content,
script.summary,
script.description ?? '',
script.kind
).toString()}
variant="border"
color="blue"
size="md"
startIcon={{ icon: faGlobe }}
>
Publish to Hub
</Button>
<Dropdown
dropdownItems={[
{
displayName: 'Use as template',
icon: faEdit,
href: `/scripts/add?template=${script.path}`
},
{
displayName: 'Share',
icon: faShare,
action: () => {
shareModal.openModal()
},
disabled: !can_write
},
{
displayName: 'Schedule',
icon: faCalendar,
href: `/schedule/add?path=${script.path}`
},
{
displayName: 'Archive',
icon: faArchive,
type: 'delete',
action: () => {
script?.hash && archiveScript(script.hash)
},
disabled: script.archived || !can_write
},
{
displayName: 'Delete',
icon: faTrash,
type: 'delete',
action: () => {
script?.hash && deleteScript(script.hash)
},
disabled: script.deleted || !($userStore?.is_admin ?? false)
}
]}
/>
</svelte:fragment>
</ActionRow>
{/if}
<CenteredPage>
<div class="flex flex-row flex-wrap justify-between gap-4">
<div>
@@ -169,115 +271,16 @@
</span>
</p>
</div>
{#if script}
<div class="flex items-start flex-wrap gap-1">
<a
class="inline-flex items-center default-button bg-transparent hover:bg-blue-500 text-blue-700 font-normal hover:text-white py-0 px-1 border-blue-500 hover:border-transparent rounded"
href="/scripts/run/{script.hash}"
>
<div class="inline-flex items-center justify-center">
<Icon class="text-blue-500" data={faPlay} scale={0.5} />
<span class="pl-1">Run</span>
</div>
</a>
<a
class="inline-flex items-center default-button bg-transparent hover:bg-blue-500 text-blue-700 font-normal hover:text-white py-0 px-1 border-blue-500 hover:border-transparent rounded"
href="/scripts/edit/{script.hash}?step=2"
class:disabled={!can_write}
>
<div class="inline-flex items-center justify-center">
<Icon class="text-blue-500" data={faEdit} scale={0.5} />
<span class="pl-1">Edit</span>
</div>
</a>
{#if !topHash}
<a
class="inline-flex items-center default-button bg-transparent hover:bg-blue-500 text-blue-700 font-normal hover:text-white py-0 px-1 border-blue-500 hover:border-transparent rounded"
href="/scripts/add?template={script.path}"
>
<div class="inline-flex items-center justify-center">
<Icon class="text-blue-500" data={faCodeFork} scale={0.5} />
<span class="pl-1">Use as template/Fork</span>
</div>
</a>
{/if}
<a
class="inline-flex items-center default-button bg-transparent hover:bg-blue-500 text-blue-700 font-normal hover:text-white py-0 px-1 border-blue-500 hover:border-transparent rounded"
href="/runs/{script.path}"
>
<div class="inline-flex items-center justify-center">
<Icon class="text-blue-500" data={faList} scale={0.5} />
<span class="pl-1">View runs</span>
</div>
</a>
<a
target="_blank"
class="inline-flex items-center default-button bg-transparent hover:bg-blue-500 text-blue-700 font-normal hover:text-white py-0 px-1 border-blue-500 hover:border-transparent rounded"
href={scriptToHubUrl(
script.content,
script.summary,
script.description ?? '',
script.kind
).toString()}
>
<div class="inline-flex items-center justify-center">
<Icon class="text-blue-500" data={faGlobe} scale={0.5} />
<span class="pl-1">Publish to Hub</span>
</div>
</a>
<Dropdown
dropdownItems={[
{
displayName: 'Use as template',
icon: faEdit,
href: `/scripts/add?template=${script.path}`
},
{
displayName: 'Share',
icon: faShare,
action: () => {
shareModal.openModal()
},
disabled: !can_write
},
{
displayName: 'Schedule',
icon: faCalendar,
href: `/schedule/add?path=${script.path}`
},
{
displayName: 'Archive',
icon: faArchive,
type: 'delete',
action: () => {
script?.hash && archiveScript(script.hash)
},
disabled: script.archived || !can_write
},
{
displayName: 'Delete',
icon: faTrash,
type: 'delete',
action: () => {
script?.hash && deleteScript(script.hash)
},
disabled: script.deleted || !($userStore?.is_admin ?? false)
}
]}
/>
</div>
{/if}
</div>
<ShareModal bind:this={shareModal} kind="script" path={script?.path ?? ''} />
<div class="flex flex-col gap-8 max-w-7xl pb-2">
<div class="flex flex-col gap-8 max-w-7xl pt-8 pb-2">
{#if script === undefined}
<p>loading</p>
{:else}
<div>
<h2 class="font-bold mt-8 mb-2">{script.summary}</h2>
<h2 class="font-bold mb-2">{script.summary}</h2>
<div class="prose">
<SvelteMarkdown source={defaultIfEmptyString(script.description, 'No description')} />
</div>
@@ -343,13 +346,15 @@
<Badge color="dark-gray" capitalize>
{type}
</Badge>
<button
on:click|preventDefault={() => copyToClipboard(url)}
class="flex items-center bg-blue-600 text-white rounded-md px-2 ml-2"
<Button
on:click={() => copyToClipboard(url)}
color="blue"
size="sm"
startIcon={{ icon: faClipboard }}
btnClasses="ml-2"
>
<Icon data={faClipboard} />
<span class="ml-1">Copy</span>
</button>
Copy
</Button>
</div>
</li>
{/each}