Merge branch 'master' into alert-polish

This commit is contained in:
Vincent Fiduccia 2020-11-23 18:35:21 -07:00 committed by GitHub
commit 997feac58d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 2270 additions and 1311 deletions

View File

@ -24,8 +24,8 @@
@import "./global/labeled-input"; @import "./global/labeled-input";
@import "./global/tooltip"; @import "./global/tooltip";
@import "./global/table"; @import "./global/table";
@import "./global/select";
/* @import 'vue-select/src/scss/vue-select.scss'; */
@import "./vendor/vue-select"; @import "./vendor/vue-select";
@import "./vendor/vue-js-modal"; @import "./vendor/vue-js-modal";
@import '@/node_modules/xterm/css/xterm.css'; @import '@/node_modules/xterm/css/xterm.css';

View File

@ -70,6 +70,8 @@ TEXTAREA,
BUTTON, BUTTON,
.btn, .btn,
.labeled-input, .labeled-input,
.labeled-select,
.unlabeled-select,
.checkbox-custom, .checkbox-custom,
.radio-custom { .radio-custom {
&:focus, &.focused { &:focus, &.focused {

View File

@ -24,7 +24,7 @@ button,
color: white; color: white;
&:hover { &:hover {
text-decoration: none; text-decoration: none;
} }
&.bg-transparent { &.bg-transparent {
@ -49,10 +49,11 @@ button,
//btn sizes //btn sizes
.btn-xs, .btn-xs,
.btn-group-xs > .btn, .btn-group-xs > .btn,
.btn-xs .btn-label { .btn-xs .btn-label,
padding: $xs-padding; .btn-xs .dd-button > .vs__dropdown-toggle {
font-size: .65em; padding: $xs-padding;
line-height: 1.5em; font-size: 0.65em;
line-height: 1.5em;
} }
.btn-group-xs > .btn { .btn-group-xs > .btn {
@ -62,8 +63,7 @@ button,
.btn-sm, .btn-sm,
.btn-group-sm > .btn, .btn-group-sm > .btn,
.btn-sm .btn-label { .btn-sm .btn-label {
padding: $sm-padding; padding: $sm-padding;
} }
.btn-group-sm > .btn { .btn-group-sm > .btn {
@ -74,8 +74,8 @@ button,
.btn-lg, .btn-lg,
.btn-group-lg > .btn, .btn-group-lg > .btn,
.btn-lg .btn-label { .btn-lg .btn-label {
padding: $lg-padding; padding: $lg-padding;
font-size: 1.2em; font-size: 1.2em;
} }
//btn roles //btn roles
@ -113,7 +113,6 @@ button,
line-height: 0; line-height: 0;
} }
.icon-group i { .icon-group i {
font-size: 1.5em; font-size: 1.5em;
} }

View File

@ -6,7 +6,7 @@ INPUT[type='password'],
INPUT[type='number'], INPUT[type='number'],
INPUT[type='date'], INPUT[type='date'],
INPUT[type='email'], INPUT[type='email'],
INPUT[type='search'], INPUT[type='search']:not(.vs__search),
INPUT[type='tel'], INPUT[type='tel'],
INPUT[type='url'], INPUT[type='url'],
SELECT, SELECT,

View File

@ -13,7 +13,6 @@
transition-timing-function: ease-in-out; transition-timing-function: ease-in-out;
color: var(--input-label); color: var(--input-label);
pointer-events: none; pointer-events: none;
z-index: z-index('overContent');
i { i {
pointer-events: initial; pointer-events: initial;
} }

View File

@ -0,0 +1,116 @@
.labeled-select {
cursor: text;
padding: 0;
width: 100%;
.selected {
padding-top: 17px;
}
}
.labeled-select,
.unlabeled-select {
min-width: 75px;
height: 100%;
.v-select {
&.inline {
.vs__search {
background-color: transparent;
}
.vs__dropdown-toggle,
.vs__dropdown-toggle > * {
background-color: transparent;
border: transparent;
}
.vs__dropdown-menu {
outline: none;
}
.selected {
position: relative;
top: 1.4em;
}
}
}
.v-select.inline.vs--single {
&.vs--searching .vs__selected {
display: none;
}
&:not(.vs--searching) {
.vs__selected-options {
overflow: hidden;
flex-wrap: nowrap;
.vs__selected {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: inline-block;
}
}
}
}
.v-select.inline:not(.vs--single) {
margin-bottom: -4px; // targets multi-select tag boxes to make the same size as rows next to it
min-height: 30px;
.vs__selected {
min-height: 25px;
padding: 0 7px;
&:not(:only-child) {
margin-bottom: 3px;
}
}
}
&.focused {
outline: none;
border: var(--outline-width) solid var(--outline);
box-shadow: none;
.v-select {
// Can toggle this to get full width dd - maybe make an option?
.vs__dropdown-menu {
min-width: max-content;
}
}
}
}
.unlabeled-select {
background-color: var(--input-bg);
border-radius: var(--border-radius);
border: solid var(--outline-width) var(--input-border);
color: var(--input-text);
.vs--single .vs__selected-options {
flex-wrap: nowrap;
}
.v-select {
&.inline {
height: 100%;
.vs__dropdown-toggle {
height: 100%;
}
.vs__actions {
width: auto;
}
}
}
&:not(.view) {
background-color: var(--input-bg);
&:hover {
&,
.vs__dropdown-menu {
background: var(--input-hover-bg);
}
}
}
}

View File

@ -180,134 +180,3 @@
transition: opacity .15s; transition: opacity .15s;
} }
} }
// load here instead of component so SSR render isn't all wonky
.dropdown-button-group {
// this matches the top/bottom padding of the default button
$trigger-padding: 15px 10px 15px 10px;
$xs-trigger-padding: 2px 4px 4px 4px;
$sm-trigger-padding: 8px 10px 10px 10px;
$lg-trigger-padding: 18px 10px 10px 10px;
.v-popover {
.text-right {
margin-top: 5px;
}
.trigger {
height: 100%;
.icon-container {
height: 100%;
padding: $trigger-padding;
i {
transform: scale(1.5);
}
&.btn-xs {
padding: $xs-trigger-padding;
}
&.btn-sm {
padding: $sm-trigger-padding;
}
&.btn-lg {
padding: $lg-trigger-padding;
}
&:focus {
outline-style: none;
box-shadow: none;
border-color: transparent;
}
}
}
}
.dropdown-button {
background: var(--tooltip-bg);
border: solid thin var(--link-text);
color: var(--link-text);
padding: 0;
display: inline-flex;
&>*, .icon-chevron-down {
color: var(--primary);
background-color: rgba(0,0,0,0);
}
.button-divider {
border-right: 1px solid var(--link-text);
}
&.bg-primary:hover {
background: var(--accent-btn-hover);
}
&.one-action {
position: relative;
&>.btn {
padding: 15px 35px 15px 15px;
}
.v-popover{
.trigger{
position: absolute;
top: 0px;
right: 0px;
left: 0px;
bottom: 0px;
BUTTON {
position: absolute;
right: 0px;
}
}
}
}
}
.popover {
border: none;
}
.tooltip {
&[x-placement^="bottom"] {
.tooltip-arrow {
border-bottom-color: var(--dropdown-border);
&:after {
border-bottom-color: var(--dropdown-bg);
}
}
}
.tooltip-inner {
color: var(--dropdown-text);
background-color: var(--dropdown-bg);
border: 1px solid var(--dropdown-border);
padding: 0px;
text-align: left;
LI {
padding: 10px 50px 10px 20px;
&.divider {
padding-top: 0px;
padding-bottom: 0px;
> .divider-inner {
padding: 0;
border-bottom: 1px solid var(--dropdown-divider);
width: 125%;
margin: 0 auto;
}
}
&:not(.divider):hover {
background-color: var(--dropdown-hover-bg);
color: var(--dropdown-hover-text);
cursor: pointer;
}
}
}
}
//header
.user-info {
border-bottom: 1px solid var(--border);
display: block;
}
}

View File

@ -54,7 +54,7 @@
--body-gradient: linear-gradient(180deg, #{$dark} 0%, #{$darkest} 100%); --body-gradient: linear-gradient(180deg, #{$dark} 0%, #{$darkest} 100%);
--body-text: #{$lightest}; --body-text: #{$lightest};
--scrollbar-thumb: #{$medium}; --scrollbar-thumb: #{$medium};
--scrollbar-thumb-dropdown: #{$darker}; --scrollbar-thumb-dropdown: #{$medium};
--nav-bg: #{$darkest}; --nav-bg: #{$darkest};
--nav-active: #{rgba($primary, 0.3)}; --nav-active: #{rgba($primary, 0.3)};

View File

@ -9,7 +9,6 @@
} }
} }
.v-select, .v-select,
.v-select * { .v-select * {
box-sizing: border-box; box-sizing: border-box;
@ -23,7 +22,6 @@
.vs__dropdown-toggle, .vs__dropdown-toggle,
.vs__clear, .vs__clear,
.vs__search, .vs__search,
// .vs__selected,
.vs__open-indicator { .vs__open-indicator {
cursor: not-allowed; cursor: not-allowed;
color: var(--dropdown-disabled-text); color: var(--dropdown-disabled-text);
@ -33,24 +31,19 @@
.vs__dropdown-menu { .vs__dropdown-menu {
display: block; display: block;
position: absolute; position: absolute;
top: calc(100% - 1px); left: -2px;
left: 0;
z-index: z-index('dropdownContent'); z-index: z-index('dropdownContent');
padding: 5px 0; padding: 5px 0;
margin: 0; margin: 0;
width: 100%; width: calc(100% + 4px);
max-height: 350px; max-height: 350px;
min-width: 160px; min-width: 160px;
overflow-y: auto; overflow-y: auto;
border: 1px solid var(--dropdown-border); border: 1px solid var(--dropdown-border);
border-top-style: none;
border-radius: var(--border-radius); border-radius: var(--border-radius);
border-top-left-radius: 0;
border-top-right-radius: 0;
text-align: left; text-align: left;
list-style: none; list-style: none;
background: var(--dropdown-bg); background: var(--dropdown-bg);
box-shadow: 0px 8px 16px 0px var(--shadow);
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-thumb-dropdown) !important; background-color: var(--scrollbar-thumb-dropdown) !important;
@ -60,7 +53,6 @@
&[data-popper-placement='top'] { &[data-popper-placement='top'] {
border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0;
border-top-style: solid; border-top-style: solid;
border-bottom-style: none;
box-shadow: 0px -8px 16px 0px var(--shadow); box-shadow: 0px -8px 16px 0px var(--shadow);
} }
} }
@ -89,8 +81,8 @@
color: var(--dropdown-disabled-text); color: var(--dropdown-disabled-text);
cursor: not-allowed; cursor: not-allowed;
HR { hr {
cursor: default cursor: default;
} }
} }
@ -104,7 +96,7 @@
color: var(--dropdown-hover-text); color: var(--dropdown-hover-text);
background: var(--dropdown-hover-bg); background: var(--dropdown-hover-bg);
A { a {
color: var(--dropdown-hover-text); color: var(--dropdown-hover-text);
text-decoration: none; text-decoration: none;
} }
@ -114,7 +106,6 @@
.vs__dropdown-toggle { .vs__dropdown-toggle {
appearance: none; appearance: none;
display: flex; display: flex;
// padding: 0 0 4px 0;
background: var(--input-bg); background: var(--input-bg);
border: 1px solid var(--dropdown-border); border: 1px solid var(--dropdown-border);
border-radius: var(--border-radius); border-radius: var(--border-radius);
@ -139,7 +130,9 @@
align-items: center; align-items: center;
pointer-events: none; pointer-events: none;
position: relative; position: relative;
left: -2px; width: 100%;
justify-content: flex-end;
flex-shrink: 8;
svg { svg {
display: none; display: none;
@ -161,32 +154,17 @@
cursor: pointer; cursor: pointer;
} }
.vs--unsearchable .vs__search { $transition-timing-function: cubic-bezier(1, -0.115, 0.975, 0.855);
background-color: var(--default-text);
width: 0px;
padding: 0;
align-self: center;
border: 0;
display: none;
}
.vs--open .vs__dropdown-toggle {
border-bottom-color: transparent;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
$transition-timing-function: cubic-bezier(1.000, -0.115, 0.975, 0.855);
$transition-duration: 150ms; $transition-duration: 150ms;
.vs__open-indicator { .vs__action:after {
fill: var(--dropdown-disabled-text); fill: var(--dropdown-disabled-text);
transform: scale(1); transform: scale(1);
transition: transform $transition-duration $transition-timing-function; transition: transform $transition-duration $transition-timing-function;
transition-timing-function: $transition-timing-function; transition-timing-function: $transition-timing-function;
} }
.vs--open .vs__open-indicator { .vs--open .vs__actions:after {
transform: rotate(180deg) scale(1); transform: rotate(180deg) scale(1);
} }
@ -199,7 +177,7 @@ $transition-duration: 150ms;
* below, the cancel button will still appear in chrome. * below, the cancel button will still appear in chrome.
* If it's up here on it's own, it'll hide it. * If it's up here on it's own, it'll hide it.
*/ */
.vs__search::-webkit-search-cancel-button { .vs__search::-webkit-search-cancel-button {
display: none; display: none;
} }
@ -237,12 +215,11 @@ $transition-duration: 150ms;
} }
} }
.vs--single.vs--searching:not(.vs--open):not(.vs--loading) { .vs--single.vs--searching:not(.vs--open):not(.vs--loading) {
.vs__search { .vs__search {
opacity: .2; opacity: 0.2;
} }
} }
/* States */ /* States */
.vs--single { .vs--single {
@ -263,7 +240,7 @@ $transition-duration: 150ms;
border: 1px solid var(--primary); border: 1px solid var(--primary);
border-radius: 3px; border-radius: 3px;
color: var(--link-text); color: var(--link-text);
padding: 6px; margin-left: 7px;
&:not(:last-of-type) { &:not(:last-of-type) {
margin-right: 2px; margin-right: 2px;
@ -296,47 +273,49 @@ $transition-duration: 150ms;
.v-select.inline { .v-select.inline {
background-color: transparent; background-color: transparent;
&.vs--single .vs__selected { &.vs--single {
color: var(--input-text); min-height: 25px;
position: absolute; &.vs--open {
top: 0; .vs__selected {
bottom:0; position: absolute;
align-self: start; opacity: 0.4;
margin: 4px; }
.vs__search {
margin-left: 7px;;
}
}
.vs__selected {
color: var(--input-text);
}
} }
.vs__dropdown-menu { .vs__dropdown-menu {
min-width: 0px; min-width: 0px;
margin-top: 2px;
} }
.vs__dropdown-toggle { .vs__dropdown-toggle {
background-color: var(--input-bg); background-color: var(--input-bg);
border:none; border: none;
height: 100%;
padding: none; padding: none;
border-radius: var(--border-radius); border-radius: var(--border-radius);
border: 1px solid var(--dropdown-border); border: 1px solid var(--dropdown-border);
} }
&.vs--single .vs__selected-options { &.vs--single .vs__selected-options {
display: flex;
flex-direction: column;
align-items: center; align-items: center;
width: 100%;
} }
.vs__search { .vs__search {
background-color: rgba(0,0,0,0); background-color: rgba(0, 0, 0, 0);
width: 100%; &:hover {
&:hover{ background-color: rgba(0, 0, 0, 0);
background-color: rgba(0,0,0,0);
} }
} }
.vs__open-indicator{ .vs__open-indicator {
fill: var(--input-label); fill: var(--input-label);
} }
.vs__clear { .vs__clear {
display:none; display: none;
} }
} }
.v-select.mini { .v-select.mini {
@ -351,15 +330,17 @@ $transition-duration: 150ms;
margin: 0; margin: 0;
} }
INPUT { input {
padding: 0; padding: 0;
} }
} }
.vs__selected-options INPUT { .vs__selected-options input {
width: auto; width: 0;
display: inline-block; display: inline-block;
border: 0; border: 0;
background-color: var(--input-bg);
color: var(--input-text);
} }
header .vs-select .vs__dropdown-toggle { header .vs-select .vs__dropdown-toggle {

View File

@ -109,6 +109,7 @@ assignTo:
=1 { Assign Cluster To… } =1 { Assign Cluster To… }
other { Assign {count} Clusters To… } other { Assign {count} Clusters To… }
} }
workspace: Workspace
asyncButton: asyncButton:
default: default:
@ -327,21 +328,41 @@ chartHeading:
cis: cis:
addTest: Add Test ID addTest: Add Test ID
alertNeeded: To receive alerts, please ensure <a tabindex="0" aria-label="Link to Rancher's Monitoring" href="{link}"> Rancher's Monitoring and Alerting app</a> is installed and the Receivers and Routes are <a target="_blank" rel='noopener nofollow' href='https://rancher.com/docs/rancher/v2.x/en/monitoring-alerting/v2.5/configuration/#alertmanager-config'> configured to send out alerts.</a>
alertNotFound: "It looks like alerting isn't configured."
alertOnComplete: Alert on scan completion
alertOnFailure: Alert on scan failure
benchmarkVersion: Benchmark Version benchmarkVersion: Benchmark Version
cronSchedule:
label: Schedule
placeholder: "e.g. 0 * * * *"
deleteProfileWarning: |-
{count, plural,
=1 { Any scheduled scans using this profile will no longer work. }
other { Any scheduled scans using either of these profiles will no longer work. }
}
downloadAllReports: Download All Saved Reports
downloadLatestReport: Download Latest Report
noProfiles: There are no valid ClusterScanProfiles for this cluster type to select. noProfiles: There are no valid ClusterScanProfiles for this cluster type to select.
profile: Profile profile: Profile
retention: Retention Count
testID: Test ID testID: Test ID
testsToSkip: Tests to Skip testsToSkip: Tests to Skip
testsSkipped: Tests Skipped
scan: scan:
description: Description description: Description
failed: Failed fail: Fail
lastScanTime: Last Scan Time lastScanTime: Last Scan Time
notApplicable: 'N/A' notApplicable: 'N/A'
number: Number number: Number
passed: Passed pass: Pass
scanReport: Scan Report scanReport: Scan Report
skipped: Skipped skip: Skip
total: Total total: Total
warn: Warn
scoreWarning:
label: Scan state for "warn" results
protip: Scans with no failures will be marked "Pass" by default even if some of the tests generate "warn" output. This behavior can be changed by selecting the "fail" option from this section.
cluster: cluster:
nodeDriver: nodeDriver:
@ -871,6 +892,24 @@ monitoring:
fields: fields:
name: Name name: Name
monitoringRoute:
groups:
label: Group By
info: This is the top-level Route used by Alertmanager as the default destination for any Alerts that do not match any other Routes. This Route must exist and cannot be deleted.
interval:
label: Group Interval
matching:
info: The root route has to match everything so matching can't be configured.
label: Match
receiver:
label: Receiver
regex:
label: Match Regex
repeatInterval:
label: Repeat Interval
wait:
label: Group Wait
nameNsDescription: nameNsDescription:
name: name:
label: Name label: Name
@ -887,6 +926,16 @@ nameNsDescription:
namespace: namespace:
containerResourceLimit: Container Resource Limit containerResourceLimit: Container Resource Limit
project:
label: Project
namespaceFilter:
selected:
label: "{total} items selected"
namespaceList:
selectLabel: Namespace
addLabel: Add Namespace
node: node:
detail: detail:
@ -957,6 +1006,39 @@ prefs:
hideDesc: hideDesc:
label: Hide All Type Description Boxes label: Hide All Type Description Boxes
probe:
checkInterval:
label: Check Interval
placeholder: 'Default: 10'
command:
label: Command to run
placeholder: e.g. cat /tmp/health
failureThreshold:
label: Failure Threshold
placeholder: 'Default: 3'
httpGet:
headers:
label: Request Headers
path:
label: Request Path
placeholder: e.g. /healthz
port:
label: Check Port
placeholder: e.g. 80
placeholderDuex: e.g. 25
initialDelay:
label: Initial Delay
placeholder: 'Default: 0'
successThreshold:
label: Success Threshold
placeholder: 'Default: 1'
timeout:
label: Timeout
placeholder: 'Default: 3'
type:
label: Type
placeholder: Select a check type
prometheusRule: prometheusRule:
alertingRules: alertingRules:
addLabel: Add Alert addLabel: Add Alert
@ -1321,6 +1403,18 @@ tableHeaders:
version: Version version: Version
weight: Weight weight: Weight
target:
router:
label: Router
placeholder: Select a router
service:
label: Service
placeholder: Select a service
title: Target
version:
label: Version
placeholder: Select a version
validation: validation:
arrayLength: arrayLength:
between: '"{key}" should contain between {min} and {max} {max, plural, =1 {item} other {items}}' between: '"{key}" should contain between {min} and {max} {max, plural, =1 {item} other {items}}'
@ -1489,6 +1583,7 @@ workload:
value: value:
label: Value label: Value
placeholder: e.g. BAR placeholder: e.g. BAR
tty: TTY
workingDir: WorkingDir workingDir: WorkingDir
stdin: Stdin stdin: Stdin
healthCheck: healthCheck:
@ -1669,6 +1764,7 @@ workload:
inNamespaces: "Pods in these namespaces:" inNamespaces: "Pods in these namespaces:"
key: Key key: Key
lessThan: < lessThan: <
namespaces: Namespaces
notIn: notIn:
operator: Operator operator: Operator
value: Value value: Value
@ -1758,6 +1854,7 @@ workload:
storagePolicyName: Storage Policy Name storagePolicyName: Storage Policy Name
volumePath: Volume Path volumePath: Volume Path
defaultMode: Default Mode defaultMode: Default Mode
driver: driver
hostPath: hostPath:
label: The Path on the Node must be label: The Path on the Node must be
options: options:
@ -2046,3 +2143,7 @@ workloadPorts:
podAffinity: podAffinity:
addLabel: Add Pod Selector addLabel: Add Pod Selector
keyValue:
keyPlaceholder: e.g. foo
valuePlaceholder: e.g. bar

View File

@ -962,7 +962,7 @@ servicePorts:
label: 协议 label: 协议
target: target:
label: 目标端口 label: 目标端口
placeholder: 例如80 or http placeholder: 例如80 http
serviceTypes: serviceTypes:
clusterip: 集群IP地址 clusterip: 集群IP地址
@ -1130,6 +1130,23 @@ tableHeaders:
value: value:
version: 版本号 version: 版本号
weight: 权重 ## doublecheck这里的权重有什么特殊意义吗 weight: 权重 ## doublecheck这里的权重有什么特殊意义吗
Capacity: 容量
'Access Modes': 访问模式
'Reclaim Policy': 重声明策略
Status: 状态
Claim: 声明
StorageClass: 存储类
Reason: 原因
VolumeMode: 存储卷模式
Targets: 目标
MinPods: 最小Pod数量
MaxPods: 最大Pod数量
Replicas: 副本数
Provisioner: 提供商
ReclaimPolicy: 重声明策略
Role: 角色
Users: 用户
Groups: 用户组
validation: validation:
arrayLength: arrayLength:
@ -1659,6 +1676,16 @@ typeLabel:
persistentvolume: 持久卷 persistentvolume: 持久卷
service: 服务 service: 服务
node: 节点 node: 节点
autoscaling.horizontalpodautoscaler: Pod水平自动伸缩
networking.k8s.io.ingress: Ingress
networking.k8s.io.networkpolicy: 网络策略
persistentvolumeclaim: 持久卷声明
storage.k8s.io.storageclass: 存储类
secret: 密钥
rbac.authorization.k8s.io.clusterrolebinding: 集群角色绑定
rbac.authorization.k8s.io.clusterrole: 集群角色
rbac.authorization.k8s.io.rolebinding: 角色绑定
rbac.authorization.k8s.io.role: 角色
action: action:
download: 下载YAML download: 下载YAML
@ -1684,3 +1711,7 @@ workloadPorts:
podAffinity: podAffinity:
addLabel: 添加 Pod Selector addLabel: 添加 Pod Selector
keyValue:
keyPlaceholder: '例如: foo'
valuePlaceholder: '例如: bar'

View File

@ -121,7 +121,7 @@ export default {
<form> <form>
<LabeledSelect <LabeledSelect
v-model="moveTo" v-model="moveTo"
label="Workspace" :label="t('assignTo.workspace')"
:options="workspaceOptions" :options="workspaceOptions"
placement="bottom" placement="bottom"
/> />
@ -141,7 +141,7 @@ export default {
<div slot="actions"> <div slot="actions">
<button class="btn role-secondary" @click="close"> <button class="btn role-secondary" @click="close">
Cancel {{ t('generic.cancel') }}
</button> </button>
<AsyncButton <AsyncButton

View File

@ -1,101 +1,235 @@
<script> <script>
import { get } from '@/utils/object';
import isString from 'lodash/isString';
import VueSelectOverrides from '@/mixins/vue-select-overrides';
export default { export default {
props: { mixins: [VueSelectOverrides],
size: { props: {
buttonLabel: {
default: '',
type: String, type: String,
default: '' // possible values are xs, sm, lg. empty is default .btn
}, },
// whether this is a button and dropdown (default) or dropdown that looks like a button/dropdown closeOnSelect: {
dualAction: { default: true,
type: Boolean
},
disabled: {
default: false,
type: Boolean, type: Boolean,
default: true },
} // array of option objects containing at least a label and link, but also icon and action are available
dropdownOptions: {
// required: true,
default: () => [],
type: Array,
},
optionKey: {
default: null,
type: String,
},
optionLabel: {
default: 'label',
type: String,
},
// sm, null(med), lg - no xs...its so small
size: {
default: null,
type: String,
},
value: {
default: null,
type: String,
},
}, },
data() {
computed: { return { focused: false };
buttonSize() {
const { size } = this;
let out;
switch (size) {
case '':
default:
out = 'btn';
break;
case 'xs':
out = 'btn btn-xs';
break;
case 'sm':
out = 'btn btn-sm';
break;
case 'lg':
out = 'btn btn-lg';
break;
}
return out;
}
}, },
methods: { methods: {
hasSlot(name = 'default') { ddButtonAction(option) {
return !!this.$slots[name] || !!this.$scopedSlots[name]; this.focusSearch();
this.$emit('dd-button-action', option);
},
getOptionLabel(option) {
if (isString(option)) {
return option;
}
if (this.$attrs['get-option-label']) {
return this.$attrs['get-option-label'](option);
}
if (get(option, this.optionLabel)) {
if (this.localizedLabel) {
return this.$store.getters['i18n/t'](get(option, this.optionLabel));
} else {
return get(option, this.optionLabel);
}
} else {
return option;
}
}, },
// allows parent components to programmatically open the dropdown onFocus() {
togglePopover() { return this.onFocusLabeled();
this.$refs.popoverButton.click();
}, },
}
onFocusLabeled() {
this.focused = true;
},
onBlur() {
return this.onBlurLabeled();
},
onBlurLabeled() {
this.focused = false;
},
focusSearch() {
this.$nextTick(() => {
this.$refs['button-dropdown'].searchEl.focus();
});
},
get,
},
}; };
</script> </script>
<template> <template>
<div class="dropdown-button-group"> <v-select
<div ref="button-dropdown"
class="dropdown-button bg-primary" class="button-dropdown btn"
:class="{'one-action':!dualAction, [buttonSize]:true}" :class="{
> disabled,
<slot v-if="dualAction" name="button-content" :buttonSize="buttonSize"> focused,
<button 'btn-sm': size === 'sm',
class="bg-transparent" 'btn-lg': size === 'lg',
:class="buttonSize" }"
disabled="true" v-bind="$attrs"
type="button" :searchable="false"
> :clearable="false"
Button :close-on-select="closeOnSelect"
</button> :filterable="false"
</slot> :value="buttonLabel"
<div :options="dropdownOptions"
v-else :map-keydown="mappedKeys"
:class="buttonSize" :get-option-key="
(opt) => (optionKey ? get(opt, optionKey) : getOptionLabel(opt))
"
:get-option-label="(opt) => getOptionLabel(opt)"
@search:blur="onBlur"
@search:focus="onFocus"
@input="$emit('click-action', $event)"
>
<template #selected-option="option">
<button
tabindex="-1"
type="button" type="button"
class="dropdown-button-two btn"
:class="{
'btn-sm': size === 'sm',
'btn-lg': size === 'lg',
}"
@click="ddButtonAction(option)"
@focus="focusSearch"
> >
<slot name="button-content" /> {{ option.label }}
</div> </button>
<div v-if="hasSlot('popover-content') && dualAction" class="button-divider"></div> </template>
<!-- Pass down templates provided by the caller -->
<v-popover <template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope">
v-if="hasSlot('popover-content')" <slot v-if="slot !== 'selected-option'" :name="slot" v-bind="scope" />
placement="bottom" </template>
:container="false" </v-select>
offset="10"
:popper-options="{modifiers: { flip: { enabled: false } } }"
>
<slot name="button-toggle-content" :buttonSize="buttonSize">
<button
ref="popoverButton"
class="icon-container bg-transparent"
:class="buttonSize"
type="button"
>
<i class="icon icon-chevron-down" />
</button>
</slot>
<template slot="popover">
<slot name="popover-content" />
</template>
</v-popover>
</div>
</div>
</template> </template>
<style lang='scss' scoped>
.button-dropdown.btn-sm {
::v-deep > .vs__dropdown-toggle {
.vs__actions {
&:after {
font-size: 1.6rem;
}
}
}
}
.button-dropdown.btn-lg {
::v-deep > .vs__dropdown-toggle {
.vs__actions {
&:after {
font-size: 2.6rem;
}
}
}
}
.button-dropdown {
background: var(--tooltip-bg);
border: solid 1px var(--link-text);
color: var(--link-text);
padding: 0;
&.vs--open ::v-deep {
outline: none;
border: var(--outline-width) solid var(--outline);
border-bottom: none;
box-shadow: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
&:hover {
::v-deep .vs__dropdown-toggle .vs__actions,
::v-deep .vs__selected-options {
background: var(--accent-btn-hover);
}
::v-deep .vs__selected-options .vs__selected button {
background-color: transparent;
}
}
::v-deep > .vs__dropdown-toggle {
width: 100%;
display: grid;
grid-template-columns: 75% 25%;
border: none;
background: transparent;
.vs__actions {
border-left: solid thin var(--link-text);
justify-content: center;
&:after {
color: var(--link-text);
}
}
}
::v-deep .vs__selected-options {
.vs__selected {
margin: unset;
border: none;
button {
border: none;
background: var(--tooltip-bg);
color: var(--link-text);
}
}
.vs__search {
// if you need to keep the dd open you can toggle these on and off
// display: none;
// visibility: hidden;
position: absolute;
opacity: 0;
padding: 0;
}
}
::v-deep .vs__dropdown-menu {
min-width: unset;
width: fit-content;
}
}
</style>

View File

@ -2,15 +2,17 @@
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import { get, isEmpty } from '@/utils/object'; import { get, isEmpty } from '@/utils/object';
import { NAMESPACE, RIO } from '@/config/types'; import { NAMESPACE, RIO } from '@/config/types';
import Card from '@/components/Card'; import InfoBox from '@/components/InfoBox';
import { alternateLabel } from '@/utils/platform'; import { alternateLabel } from '@/utils/platform';
import LinkDetail from '@/components/formatter/LinkDetail'; import LinkDetail from '@/components/formatter/LinkDetail';
import { uniq } from '@/utils/array'; import { uniq } from '@/utils/array';
export default { export default {
components: { Card, LinkDetail }, components: { InfoBox, LinkDetail },
data() { data() {
return { confirmName: '', error: '' }; return {
confirmName: '', error: '', warning: '', preventDelete: false
};
}, },
computed: { computed: {
names() { names() {
@ -54,20 +56,6 @@ export default {
return (type === NAMESPACE || type === RIO.STACK) && this.toRemove.length === 1; return (type === NAMESPACE || type === RIO.STACK) && this.toRemove.length === 1;
}, },
preventDeletionMessage() {
const toRemoveWithWarning = this.toRemove.filter(tr => tr?.preventDeletionMessage);
if (toRemoveWithWarning.length === 0) {
return null;
}
return toRemoveWithWarning[0].preventDeletionMessage;
},
isDeleteDisabled() {
return !!this.preventDeletionMessage;
},
plusMore() { plusMore() {
const remaining = this.toRemove.length - this.names.length; const remaining = this.toRemove.length - this.names.length;
@ -126,6 +114,31 @@ export default {
} else { } else {
this.$modal.hide('promptRemove'); this.$modal.hide('promptRemove');
} }
},
// check for any resources with a deletion prevention message,
// if none found (delete is allowed), then check for any resources with a warning message
toRemove(neu) {
let message;
const preventDeletionMessages = neu.filter(item => item.preventDeletionMessage);
if (!!preventDeletionMessages.length) {
this.preventDelete = true;
message = preventDeletionMessages[0].preventDeletionMessage;
} else {
const warnDeletionMessages = neu.filter(item => item.warnDeletionMessage);
if (!!warnDeletionMessages.length) {
message = warnDeletionMessages[0].warnDeletionMessage;
}
}
if (typeof message === 'function' ) {
this.warning = message(this.toRemove);
} else if (!!message) {
this.warning = message;
} else {
this.warning = '';
}
} }
}, },
@ -219,7 +232,7 @@ export default {
height="auto" height="auto"
styles="background-color: var(--nav-bg); border-radius: var(--border-radius); max-height: 100vh;" styles="background-color: var(--nav-bg); border-radius: var(--border-radius); max-height: 100vh;"
> >
<Card> <InfoBox>
<h4 slot="title" class="text-default-text"> <h4 slot="title" class="text-default-text">
Are you sure? Are you sure?
</h4> </h4>
@ -231,10 +244,13 @@ export default {
<span v-if="i===names.length-1" :key="resource+2">{{ plusMore }}</span><span v-else :key="resource+1">{{ i === toRemove.length-2 ? ', and ' : ', ' }}</span> <span v-if="i===names.length-1" :key="resource+2">{{ plusMore }}</span><span v-else :key="resource+1">{{ i === toRemove.length-2 ? ', and ' : ', ' }}</span>
</template> </template>
</template> </template>
<span class="text-warning">
{{ warning }}
</span>
<span v-if="needsConfirm" :key="resource">Re-enter its name below to confirm:</span> <span v-if="needsConfirm" :key="resource">Re-enter its name below to confirm:</span>
</div> </div>
<input v-if="needsConfirm" id="confirm" v-model="confirmName" type="text" /> <input v-if="needsConfirm" id="confirm" v-model="confirmName" type="text" />
<span class="text-warning">{{ preventDeletionMessage }}</span>
<span class="text-error">{{ error }}</span> <span class="text-error">{{ error }}</span>
<span v-if="!needsConfirm" class="text-info mt-20">{{ protip }}</span> <span v-if="!needsConfirm" class="text-info mt-20">{{ protip }}</span>
</div> </div>
@ -242,11 +258,11 @@ export default {
<button class="btn role-secondary" @click="close"> <button class="btn role-secondary" @click="close">
Cancel Cancel
</button> </button>
<button class="btn bg-error" :disabled="isDeleteDisabled" @click="remove"> <button class="btn bg-error" :disabled="preventDelete" @click="remove">
Delete Delete
</button> </button>
</template> </template>
</Card> </InfoBox>
</modal> </modal>
</template> </template>

View File

@ -1,31 +1,30 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import Favorite from '@/components/nav/Favorite'; import Favorite from '@/components/nav/Favorite';
import ButtonDropdown from '@/components/ButtonDropdown';
import TypeDescription from '@/components/TypeDescription'; import TypeDescription from '@/components/TypeDescription';
import { get } from '@/utils/object';
export default { export default {
components: { components: {
ButtonDropdown,
Favorite, Favorite,
TypeDescription, TypeDescription,
}, },
props: { props: {
resource: { resource: {
type: String, type: String,
required: true required: true,
}, },
schema: { schema: {
type: Object, type: Object,
default: null default: null,
}, },
typeDisplay: { typeDisplay: {
type: String, type: String,
default: '' default: '',
}, },
isCreatable: { isCreatable: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
isYamlCreatable: { isYamlCreatable: {
type: Boolean, type: Boolean,
@ -33,19 +32,20 @@ export default {
}, },
createLocation: { createLocation: {
type: Object, type: Object,
default: null default: null,
}, },
yamlCreateLocation: { yamlCreateLocation: {
type: Object, type: Object,
default: null default: null,
} },
}, },
computed: { computed: {
get,
...mapGetters(['isExplorer']), ...mapGetters(['isExplorer']),
resourceName() { resourceName() {
if ( this.schema ) { if (this.schema) {
return this.$store.getters['type-map/labelFor'](this.schema); return this.$store.getters['type-map/labelFor'](this.schema);
} }
@ -62,64 +62,26 @@ export default {
{{ typeDisplay }} <Favorite v-if="isExplorer" :resource="resource" /> {{ typeDisplay }} <Favorite v-if="isExplorer" :resource="resource" />
</h1> </h1>
<div class="actions"> <div class="actions">
<ButtonDropdown <nuxt-link
v-if="isCreatable || isYamlCreatable" v-if="isCreatable"
:to="createLocation"
class="btn role-primary"
> >
<template #button-content="slotProps"> {{ t('resourceList.head.create') }}
<nuxt-link </nuxt-link>
v-if="isCreatable" <nuxt-link
:to="createLocation" v-if="isYamlCreatable"
class="btn bg-transparent" :to="yamlCreateLocation"
:class="slotProps.buttonSize" class="btn role-primary"
> >
{{ t("resourceList.head.create") }} {{ t('resourceList.head.createFromYaml') }}
</nuxt-link> </nuxt-link>
<nuxt-link
v-else-if="!isCreatable && isYamlCreatable"
:to="yamlCreateLocation"
class="btn bg-transparent"
:class="slotProps.buttonSize"
>
{{ t("resourceList.head.createFromYaml") }}
</nuxt-link>
<a
v-else
href="#"
class="btn bg-transparent"
:class="slotProps.buttonSize"
disabled="true"
@click.prevent.self
>
{{ t("resourceList.head.create") }}
</a>
</template>
<template
v-if="isCreatable && isYamlCreatable"
slot="popover-content"
>
<ul class="list-unstyled menu" style="margin: -1px;">
<li class="hand">
<nuxt-link
v-if="isCreatable"
:to="createLocation"
>
{{ t("resourceList.head.createResource", { resourceName }) }}
</nuxt-link>
</li>
<li class="divider">
<div class="divider-inner"></div>
</li>
<li class="hand">
<nuxt-link
v-if="isYamlCreatable"
:to="yamlCreateLocation"
>
{{ t("resourceList.head.createFromYaml") }}
</nuxt-link>
</li>
</ul>
</template>
</ButtonDropdown>
</div> </div>
</header> </header>
</template> </template>
<style lang="scss" scoped>
.actions {
grid-column: max-content;
}
</style>

View File

@ -72,8 +72,10 @@ export default {
}, },
addLabel: { addLabel: {
type: String, type: String,
default: 'Add', default() {
return this.$store.getters['i18n/t']('generic.add');
},
}, },
addIcon: { addIcon: {
type: String, type: String,
@ -85,8 +87,10 @@ export default {
}, },
removeLabel: { removeLabel: {
type: String, type: String,
default: '', default() {
return this.$store.getters['i18n/t']('generic.remove');
},
}, },
removeIcon: { removeIcon: {
type: String, type: String,
@ -295,7 +299,6 @@ export default {
<div v-if="showRemove" class="remove"> <div v-if="showRemove" class="remove">
<slot name="remove-button" :remove="() => remove(idx)"> <slot name="remove-button" :remove="() => remove(idx)">
<button type="button" class="btn role-link" @click="remove(idx)"> <button type="button" class="btn role-link" @click="remove(idx)">
Remove
{{ removeLabel }} {{ removeLabel }}
</button> </button>
</slot> </slot>

View File

@ -12,7 +12,7 @@ export default {
ShellInput, ShellInput,
LabeledSelect, LabeledSelect,
Checkbox, Checkbox,
EnvVars EnvVars,
}, },
props: { props: {
@ -21,30 +21,40 @@ export default {
required: true, required: true,
}, },
configMaps: { configMaps: {
type: Array, type: Array,
default: () => [] default: () => [],
}, },
secrets: { secrets: {
type: Array, type: Array,
default: () => [] default: () => [],
}, },
// container spec // container spec
value: { value: {
type: Object, type: Object,
default: () => { default: () => {
return {}; return {};
} },
} },
}, },
data() { data() {
const { const {
command, args, workingDir, stdin = false, stdinOnce = false, tty = false command,
args,
workingDir,
stdin = false,
stdinOnce = false,
tty = false,
} = this.value; } = this.value;
return { return {
command, args, workingDir, stdin, stdinOnce, tty args,
command,
commandOptions: ['No', 'Once', 'Yes'],
stdin,
stdinOnce,
tty,
workingDir,
}; };
}, },
@ -78,8 +88,8 @@ export default {
break; break;
} }
this.update(); this.update();
} },
} },
}, },
methods: { methods: {
@ -93,10 +103,10 @@ export default {
args: this.args, args: this.args,
workingDir: this.workingDir, workingDir: this.workingDir,
tty: this.tty, tty: this.tty,
}) }),
}; };
this.$emit('input', out ); this.$emit('input', out);
}, },
}, },
}; };
@ -136,18 +146,33 @@ export default {
/> />
</div> </div>
<div class="col span-6"> <div class="col span-6">
<div :style="{'align-items':'center'}" class="row"> <div :style="{ 'align-items': 'center' }" class="row">
<div class="col span-6"> <div class="col span-6">
<LabeledSelect v-model="stdinSelect" :label="t('workload.container.command.stdin')" :options="['No', 'Once', 'Yes']" :mode="mode" /> <LabeledSelect
v-model="stdinSelect"
:label="t('workload.container.command.stdin')"
:options="commandOptions"
:mode="mode"
/>
</div> </div>
<div v-if="stdin" class="col span-6"> <div v-if="stdin" class="col span-6">
<Checkbox v-model="tty" :mode="mode" label="TTY" @input="update" /> <Checkbox
v-model="tty"
:mode="mode"
:label="t('workload.container.command.stdin')"
@input="update"
/>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="spacer"></div> <div class="spacer"></div>
<h3>{{ t('workload.container.titles.env') }}</h3> <h3>{{ t('workload.container.titles.env') }}</h3>
<EnvVars :mode="mode" :config-maps="configMaps" :secrets="secrets" :value="value" /> <EnvVars
:mode="mode"
:config-maps="configMaps"
:secrets="secrets"
:value="value"
/>
</div> </div>
</template> </template>

View File

@ -112,8 +112,16 @@ export default {
</div> </div>
</template> </template>
<style lang='scss'> <style lang='scss' scoped>
.value-from ::v-deep {
.v-select {
height: 50px;
}
INPUT:not(.vs__search) {
height: 50px;
}
}
.value-from, .value-from-headers { .value-from, .value-from-headers {
display: grid; display: grid;
grid-template-columns: 20% 20% 20% 5% 20% auto; grid-template-columns: 20% 20% 20% 5% 20% auto;

View File

@ -3,6 +3,7 @@ import { mapState } from 'vuex';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { findBy } from '@/utils/array'; import { findBy } from '@/utils/array';
import { EXTENDED_SCOPES } from '@/store/github'; import { EXTENDED_SCOPES } from '@/store/github';
import LabeledSelect from '@/components/form/LabeledSelect';
export const FILE_PATTERNS = { export const FILE_PATTERNS = {
dockerfile: /^Dockerfile(\..*)?$/i, dockerfile: /^Dockerfile(\..*)?$/i,
@ -11,7 +12,8 @@ export const FILE_PATTERNS = {
}; };
export default { export default {
props: { components: { LabeledSelect },
props: {
value: { value: {
type: Object, type: Object,
required: true, required: true,
@ -280,7 +282,7 @@ export default {
</div> </div>
<div class="row"> <div class="row">
<div class="col span-4"> <div class="col span-4">
<v-select <LabeledSelect
:placeholder="repoPlaceholder" :placeholder="repoPlaceholder"
:disabled="loadingRecentRepos" :disabled="loadingRecentRepos"
:options="repos" :options="repos"
@ -310,10 +312,10 @@ export default {
{{ option.full_name }} {{ option.full_name }}
</div> </div>
</template> </template>
</v-select> </LabeledSelect>
</div> </div>
<div class="col span-4"> <div class="col span-4">
<v-select <LabeledSelect
:disabled="!selectedRepo || loadingBranches" :disabled="!selectedRepo || loadingBranches"
:placeholder="branchPlaceholder" :placeholder="branchPlaceholder"
:options="branches" :options="branches"
@ -322,10 +324,10 @@ export default {
:clearable="false" :clearable="false"
@input="selectBranch" @input="selectBranch"
> >
</v-select> </LabeledSelect>
</div> </div>
<div class="col span-4"> <div class="col span-4">
<v-select <LabeledSelect
:disabled="!selectedBranch" :disabled="!selectedBranch"
:placeholder="filePlaceholder" :placeholder="filePlaceholder"
:options="files" :options="files"
@ -334,7 +336,7 @@ export default {
:clearable="false" :clearable="false"
@input="selectFile" @input="selectFile"
> >
</v-select> </LabeledSelect>
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,13 +2,17 @@
import labeledFormElement from '@/mixins/labeled-form-element'; import labeledFormElement from '@/mixins/labeled-form-element';
import LabeledInput from '@/components/form/LabeledInput'; import LabeledInput from '@/components/form/LabeledInput';
import LabeledSelect from '@/components/form/LabeledSelect'; import LabeledSelect from '@/components/form/LabeledSelect';
import Select from '@/components/form/Select';
import UnitInput from '@/components/form/UnitInput'; import UnitInput from '@/components/form/UnitInput';
export default { export default {
components: { components: {
LabeledInput, LabeledSelect, UnitInput LabeledInput,
LabeledSelect,
UnitInput,
Select,
}, },
mixins: [labeledFormElement], mixins: [labeledFormElement],
props: { props: {
disabled: { disabled: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -26,22 +30,22 @@ export default {
selectLabel: { selectLabel: {
type: String, type: String,
default: '' default: '',
}, },
selectValue: { selectValue: {
type: String, type: String,
default: null default: null,
}, },
optionLabel: { optionLabel: {
type: String, type: String,
default: 'label' default: 'label',
}, },
options: { options: {
type: Array, type: Array,
required: true required: true,
}, },
selectBeforeText: { selectBeforeText: {
@ -51,34 +55,37 @@ export default {
textLabel: { textLabel: {
type: String, type: String,
default: '' default: '',
}, },
textRequired: { textRequired: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
textValue: { textValue: {
type: [String, Number], type: [String, Number],
default: '' default: '',
}, },
placeholder: { placeholder: {
type: String, type: String,
default: '' default: '',
}, },
}, },
data() { data() {
return { selected: this.selectValue || this.options[0], string: this.textValue }; return {
selected: this.selectValue || this.options[0],
string: this.textValue,
};
}, },
methods: { methods: {
focus() { focus() {
const comp = this.$refs.text; const comp = this.$refs.text;
if ( comp ) { if (comp) {
comp.focus(); comp.focus();
} }
}, },
@ -86,29 +93,57 @@ export default {
change() { change() {
this.$emit('input', { selected: this.selected, text: this.string }); this.$emit('input', { selected: this.selected, text: this.string });
}, },
} },
}; };
</script> </script>
<template> <template>
<div v-if="isView && !selectBeforeText"> <div v-if="isView && !selectBeforeText">
<UnitInput mode="view" :value="string" :label="textLabel" :suffix="selected" /> <UnitInput
mode="view"
:value="string"
:label="textLabel"
:suffix="selected"
/>
</div> </div>
<div v-else :class="{'select-after':!selectBeforeText}" class="input-container row" @input="change"> <div
v-else
:class="{ 'select-after': !selectBeforeText }"
class="input-container row"
@input="change"
>
<LabeledSelect <LabeledSelect
v-if="selectLabel"
v-model="selected" v-model="selected"
:label="selectLabel" :label="selectLabel"
:class="{'in-input': !isView}" :class="{ 'in-input': !isView }"
:options="options" :options="options"
:searchable="searchable" :searchable="false"
:disbaled="isView"
:clearable="false" :clearable="false"
:disabled="disabled" :disabled="disabled || isView"
:taggable="taggable" :taggable="taggable"
:create-option="name => ({ label: name, value: name })" :create-option="(name) => ({ label: name, value: name })"
:multiple="false" :multiple="false"
:mode="mode" :mode="mode"
:option-label="optionLabel" :option-label="optionLabel"
:placement="$attrs.placement ? $attrs.placement : null"
:v-bind="$attrs"
@input="change"
/>
<Select
v-else
v-model="selected"
:options="options"
:searchable="searchable"
:disabled="disabled || isView"
:clearable="false"
:class="{ 'in-input': !isView }"
:taggable="taggable"
:create-option="(name) => ({ label: name, value: name })"
:multiple="false"
:mode="mode"
:option-label="optionLabel"
:placement="$attrs.placement ? $attrs.placement : null"
:v-bind="$attrs" :v-bind="$attrs"
@input="change" @input="change"
/> />
@ -143,112 +178,82 @@ export default {
</div> </div>
</template> </template>
<style lang='scss'> <style lang='scss' scoped>
.input-container { .input-container {
display: flex; display: flex;
height: 52px; height: 52px;
&.select-after { &.select-after {
flex-direction: row-reverse; flex-direction: row-reverse;
& .input-string {
border-radius: var(--border-radius) 0 0 var(--border-radius);
border-right: 0;
border-left: 1px solid var(--border);
}
& .in-input {
border-radius: 0 var(--border-radius) var(--border-radius) 0;
border-left: 0;
border-right: 1px solid var(--border);
&.labeled-select {
width: 20%;
.selected{
color: var(--input-text);
text-align: center;
margin-right: 1em;
}
}
}
}
& .input-string { & .input-string {
padding-right: 8px; border-radius: var(--border-radius) 0 0 var(--border-radius);
height: 100%; border-right: 0;
width:60%; border-left: 1px solid var(--border);
flex-grow: 1;
border-radius: 0 var(--border-radius) var(--border-radius) 0;
border-left: 0;
margin-left: -1px;
display: initial;
} }
.in-input { & .in-input {
margin-right: 0; border-radius: 0 var(--border-radius) var(--border-radius) 0;
border-radius: var(--border-radius) 0 0 var(--border-radius); border-left: 0;
border-right: 1px solid var(--border);
&.labeled-select { &.labeled-select {
display: block; .selected {
color: var(--input-text);
text-align: center;
margin-right: 1em;
}
}
}
}
& .input-string {
padding-right: 0;
height: 100%;
width: 60%;
flex-grow: 1;
border-radius: 0 var(--border-radius) var(--border-radius) 0;
border-left: 0;
margin-left: -1px;
display: initial;
}
& .in-input {
margin-right: 0;
border-radius: var(--border-radius) 0 0 var(--border-radius);
&.labeled-select.focused ::v-deep,
&.unlabeled-select.focused ::v-deep {
outline: none;
border: var(--outline-width) solid var(--outline);
}
&.labeled-select ::v-deep,
&.unlabeled-select ::v-deep {
box-shadow: none;
width: 20%;
background-color: var(--accent-btn);
z-index: 1;
border: solid 1px var(--primary);
.vs__selected {
color: var(--input-text);
}
.vs__dropdown-menu {
box-shadow: none; box-shadow: none;
width: 40%; .vs__dropdown-option {
padding: 3px 5px;
}
}
.vs__dropdown-toggle {
color: var(--primary) !important;
height: 100%; height: 100%;
background-color: var(--accent-btn); border-radius: var(--border-radius) 0 0 var(--border-radius);
border: solid 1px var(--primary);
position: relative;
z-index: 1;
.vs__selected {
margin: 0;
color: var(--input-text)
}
.vs__dropdown-menu {
width: calc(100% + 2px);
left: -1px;
box-shadow: none;
border: 1px solid var(--primary);
.vs__dropdown-option {
padding: 3px 5px;
}
}
.vs__dropdown-toggle {
color: var(--primary) !important;
height: 100%;
padding: none;
display: flex;
align-items: stretch;
border-radius: var(--border-radius) 0 0 var(--border-radius);
& * {
padding: 0
}
}
.vs__selected-options {
display: -webkit-box;
& .labeled-input {
top:10px;
& LABEL, .selected {
color: var(--primary);
}
}
}
.vs__actions {
padding: 2px;;
}
.vs__open-indicator {
fill: var(--primary);
transform: scale(0.75);
}
&.vs--open .vs__open-indicator {
transform: rotate(180deg) scale(0.75);
}
} }
} }
} }
}
</style> </style>

View File

@ -11,7 +11,6 @@ import ClickExpand from '@/components/formatter/ClickExpand';
import { get } from '@/utils/object'; import { get } from '@/utils/object';
import CodeMirror from '@/components/CodeMirror'; import CodeMirror from '@/components/CodeMirror';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import ButtonDropdown from '@/components/ButtonDropdown';
import FileSelector from '@/components/form/FileSelector'; import FileSelector from '@/components/form/FileSelector';
import { HIDE_SENSITIVE } from '@/store/prefs'; import { HIDE_SENSITIVE } from '@/store/prefs';
@ -22,7 +21,6 @@ export default {
TextAreaAutoGrow, TextAreaAutoGrow,
ClickExpand, ClickExpand,
CodeMirror, CodeMirror,
ButtonDropdown,
FileSelector FileSelector
}, },
@ -74,8 +72,10 @@ export default {
}, },
}, },
keyPlaceholder: { keyPlaceholder: {
type: String, type: String,
default: 'e.g. foo' default() {
return this.$store.getters['i18n/t']('keyValue.valuePlaceholder');
},
}, },
separatorLabel: { separatorLabel: {
@ -95,8 +95,10 @@ export default {
}, },
}, },
valuePlaceholder: { valuePlaceholder: {
type: String, type: String,
default: 'e.g. bar' default() {
return this.$store.getters['i18n/t']('keyValue.valuePlaceholder');
},
}, },
valueCanBeEmpty: { valueCanBeEmpty: {
type: Boolean, type: Boolean,
@ -524,21 +526,10 @@ export default {
<div v-if="!titleAdd && (showAdd || showRead)" class="footer"> <div v-if="!titleAdd && (showAdd || showRead)" class="footer">
<slot name="add" :add="add"> <slot name="add" :add="add">
<ButtonDropdown size="sm"> <button v-if="showAdd" type="button" class="btn btn-sm role-secondary add" @click="add()">
<template #button-content> {{ addLabel }}
<button v-if="showAdd" type="button" class="btn btn-sm add" @click="add()"> </button>
{{ addLabel }} <FileSelector v-if="showRead" class="btn-sm role-secondary" :label="t('generic.readFromFile')" :include-file-name="true" @selected="onFileSelected" />
</button>
<FileSelector v-else class="btn-sm" :label="t('generic.readFromFile')" :include-file-name="true" @selected="onFileSelected" />
</template>
<template v-if="showRead && showAdd" #popover-content>
<ul class="list-unstyled">
<li>
<FileSelector class="btn-sm role-link" :label="readLabel" :include-file-name="true" @selected="onFileSelected" />
</li>
</ul>
</template>
</ButtonDropdown>
</slot> </slot>
</div> </div>
</div> </div>
@ -566,7 +557,7 @@ export default {
-ms-word-break: break-all; -ms-word-break: break-all;
word-break: break-word; word-break: break-word;
display:flex; display:flex;
align-items:start; align-items:flex-start;
} }
&.extra-column { &.extra-column {
@ -581,7 +572,7 @@ export default {
width: 100%; width: 100%;
margin: 10px 0px 10px 0px; margin: 10px 0px 10px 0px;
&.key { &.key {
align-self: start; align-self: flex-start;
} }
.text-monospace:not(.conceal) { .text-monospace:not(.conceal) {

View File

@ -4,10 +4,11 @@ import LabeledFormElement from '@/mixins/labeled-form-element';
import { findBy } from '@/utils/array'; import { findBy } from '@/utils/array';
import { get } from '@/utils/object'; import { get } from '@/utils/object';
import LabeledTooltip from '@/components/form/LabeledTooltip'; import LabeledTooltip from '@/components/form/LabeledTooltip';
import VueSelectOverrides from '@/mixins/vue-select-overrides';
export default { export default {
components: { LabeledTooltip }, components: { LabeledTooltip },
mixins: [LabeledFormElement], mixins: [LabeledFormElement, VueSelectOverrides],
props: { props: {
value: { value: {
@ -24,15 +25,15 @@ export default {
}, },
disabled: { disabled: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
optionKey: { optionKey: {
type: String, type: String,
default: null default: null,
}, },
optionLabel: { optionLabel: {
type: String, type: String,
default: 'label' default: 'label',
}, },
placement: { placement: {
type: String, type: String,
@ -40,30 +41,34 @@ export default {
}, },
tooltip: { tooltip: {
type: String, type: String,
default: null default: null,
}, },
hoverTooltip: { hoverTooltip: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
localizedLabel: { localizedLabel: {
type: Boolean, type: Boolean,
default: false default: false,
},
searchable: {
default: false,
type: Boolean,
}, },
status: { status: {
type: String, type: String,
default: null default: null,
}, },
reduce: { reduce: {
type: Function, type: Function,
default: (e) => { default: (e) => {
if ( e && typeof e === 'object' && e.value !== undefined ) { if (e && typeof e === 'object' && e.value !== undefined) {
return e.value; return e.value;
} }
return e; return e;
} },
} },
}, },
data() { data() {
@ -74,15 +79,15 @@ export default {
currentLabel() { currentLabel() {
let entry; let entry;
if ( this.grouped ) { if (this.grouped) {
for ( let i = 0 ; i < this.options.length && !entry ; i++ ) { for (let i = 0; i < this.options.length && !entry; i++) {
entry = findBy(this.options[i].items || [], 'value', this.value); entry = findBy(this.options[i].items || [], 'value', this.value);
} }
} else { } else {
entry = findBy(this.options || [], 'value', this.value); entry = findBy(this.options || [], 'value', this.value);
} }
if ( entry ) { if (entry) {
return entry.label; return entry.label;
} }
@ -91,6 +96,11 @@ export default {
}, },
methods: { methods: {
focusSearch() {
this.$nextTick(() => {
this.$refs.input.searchEl.focus();
});
},
onFocus() { onFocus() {
this.selectedVisibility = 'hidden'; this.selectedVisibility = 'hidden';
this.onFocusLabeled(); this.onFocusLabeled();
@ -102,6 +112,9 @@ export default {
}, },
getOptionLabel(option) { getOptionLabel(option) {
if (!option) {
return;
}
if (this.$attrs['get-option-label']) { if (this.$attrs['get-option-label']) {
return this.$attrs['get-option-label'](option); return this.$attrs['get-option-label'](option);
} }
@ -138,7 +151,7 @@ export default {
modifiers: [ modifiers: [
{ {
name: 'offset', name: 'offset',
options: { offset: [0, -1] } options: { offset: [0, 2] },
}, },
{ {
name: 'toggleClass', name: 'toggleClass',
@ -147,7 +160,8 @@ export default {
fn({ state }) { fn({ state }) {
component.$el.setAttribute('x-placement', state.placement); component.$el.setAttribute('x-placement', state.placement);
}, },
}] },
],
}); });
/** /**
@ -163,26 +177,42 @@ export default {
input.open = true; input.open = true;
} }
}, },
get get,
}, },
}; };
</script> </script>
<template> <template>
<div class="labeled-select labeled-input" :class="{disabled: disabled && !isView, focused, [mode]: true, [status]: status, taggable: $attrs.taggable, hoverable: hoverTooltip }"> <div
<div :class="{'labeled-container': true, raised, empty, [mode]: true}" :style="{border:'none'}"> class="labeled-select"
<label v-if="label"> :class="{
disabled: disabled && !isView,
focused,
[mode]: true,
[status]: status,
taggable: $attrs.taggable,
hoverable: hoverTooltip,
}"
@click="focusSearch"
@focus="focusSearch"
>
<div
:class="{ 'labeled-container': true, raised, empty, [mode]: true }"
:style="{ border: 'none' }"
>
<label>
{{ label }} {{ label }}
<span v-if="required && !value" class="required">*</span> <span v-if="required && !value" class="required">*</span>
</label> </label>
<label v-if="label" class="corner"> <div
<slot name="corner" /> v-if="isView"
</label> :class="{ 'no-label': !(label || '').length }"
<div v-if="isView" :class="{'no-label':!(label||'').length}" class="selected"> class="selected"
<span v-if="!currentLabel" class="text-muted"></span>{{ currentLabel }}&nbsp; >
</div> <span v-if="!currentLabel" class="text-muted">
<div v-else-if="!$attrs.multiple" :class="{'no-label':!(label||'').length}" class="selected" :style="{visibility:selectedVisibility}"> {{ currentLabel }}&nbsp;
{{ currentLabel }}&nbsp; </span>
<span v-else class="text-muted"></span>
</div> </div>
</div> </div>
<v-select <v-select
@ -192,123 +222,105 @@ export default {
class="inline" class="inline"
:append-to-body="!!placement" :append-to-body="!!placement"
:calculate-position="placement ? withPopper : undefined" :calculate-position="placement ? withPopper : undefined"
:class="{'no-label':!(label||'').length}" :class="{ 'no-label': !(label || '').length, }"
:disabled="isView || disabled" :disabled="isView || disabled"
:get-option-key="opt=>optionKey ? get(opt, optionKey) : getOptionLabel(opt)" :get-option-key="(opt) => (optionKey ? get(opt, optionKey) : getOptionLabel(opt))"
:get-option-label="opt=>getOptionLabel(opt)" :get-option-label="(opt) => getOptionLabel(opt)"
:label="optionLabel" :label="optionLabel"
:options="options" :options="options"
:map-keydown="mappedKeys"
:placeholder="placeholder" :placeholder="placeholder"
:reduce="x => reduce(x)" :reduce="(x) => reduce(x)"
:searchable="isSearchable"
:value="value != null ? value : ''" :value="value != null ? value : ''"
@input="e=>$emit('input', e)" @input="(e) => $emit('input', e)"
@search:blur="onBlur" @search:blur="onBlur"
@search:focus="onFocus" @search:focus="onFocus"
> >
<template v-if="!$attrs.multiple" v-slot:selected-option-container>
<span style="display: none"></span>
</template>
<!-- Pass down templates provided by the caller --> <!-- Pass down templates provided by the caller -->
<template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope"> <template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope">
<slot :name="slot" v-bind="scope" /> <slot :name="slot" v-bind="scope" />
</template> </template>
</v-select> </v-select>
<LabeledTooltip v-if="tooltip && !focused" :hover="hoverTooltip" :value="tooltip" :status="status" /> <LabeledTooltip
v-if="tooltip && !focused"
:hover="hoverTooltip"
:value="tooltip"
:status="status"
/>
</div> </div>
</template> </template>
<style lang='scss'> <style lang='scss' scoped>
.labeled-select { .labeled-select {
&.hoverable .v-select *{ &.hoverable ::v-deep {
z-index: z-index('overContent') .v-select * {
} z-index: z-index('overContent');
.labeled-container .selected { }
background-color: transparent;
} }
&.view.labeled-input .labeled-container { .labeled-container {
padding: 0; padding: 8px 0 0 8px;
label {
margin: 0;
}
.selected {
background-color: transparent;
}
} }
&.disabled { &.view {
.labeled-container, .vs__dropdown-toggle, input, label { &.labeled-input {
.labeled-container {
padding: 0;
}
}
}
::v-deep .vs__selected-options {
margin-top: -4px;
}
::v-deep .v-select:not(.vs--single) {
.vs__selected-options {
padding: 5px 0;
}
}
::v-deep .vs__actions {
&:after {
line-height: 1.85rem;
position: relative;
right: 3px;
top: -10px;
}
}
::v-deep &.disabled {
.labeled-container,
.vs__dropdown-toggle,
input,
label {
cursor: not-allowed; cursor: not-allowed;
} }
} }
.selected { .no-label ::v-deep {
padding-top: 17px;
}
.selected, .vs__selected-options {
.vs__search, .vs__search:hover {
background-color: transparent;
padding: 2px 0 0 0;
flex: 1;
}
}
.no-label {
&.v-select:not(.vs--single) { &.v-select:not(.vs--single) {
min-height: 33px; min-height: 33px;
} }
&.selected { &.selected {
padding-top:8px; padding-top: 8px;
padding-bottom: 9px; padding-bottom: 9px;
position: relative; position: relative;
max-height:2.3em; max-height: 2.3em;
overflow:hidden; overflow: hidden;
} }
.vs__selected-options { .vs__selected-options {
padding:8px 0 7px 0; padding: 8px 0 7px 0;
}
}
&.focused .vs__dropdown-menu {
outline: none;
border: var(--outline-width) solid var(--outline);
border-top: none;
}
&.taggable {
.vs__selected-options {
margin: 14px 0px 2px 0px;
}
}
.v-select.inline {
position: initial;
&.vs--single {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
.vs__search {
background-color: transparent;
padding: 18px 0 0 10px;
}
}
&, .vs__dropdown-toggle, .vs__dropdown-toggle > * {
background-color: transparent;
border:transparent;
}
.vs__dropdown-menu {
top: calc(100% - 2px);
left: -3px;
width: calc(100% + 6px);
}
.selected{
position:relative;
top: 1.4em;
} }
} }
} }

View File

@ -2,6 +2,7 @@
import { NODE, POD, NAMESPACE } from '@/config/types'; import { NODE, POD, NAMESPACE } from '@/config/types';
import LabeledInput from '@/components/form/LabeledInput'; import LabeledInput from '@/components/form/LabeledInput';
import LabeledSelect from '@/components/form/LabeledSelect'; import LabeledSelect from '@/components/form/LabeledSelect';
import Select from '@/components/form/Select';
import { sortBy } from '@/utils/sort'; import { sortBy } from '@/utils/sort';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { removeObject } from '@/utils/array'; import { removeObject } from '@/utils/array';
@ -10,6 +11,7 @@ export default {
components: { components: {
LabeledInput, LabeledInput,
LabeledSelect, LabeledSelect,
Select,
}, },
props: { props: {
// array of match expressions // array of match expressions
@ -232,7 +234,7 @@ export default {
<div v-if="isView"> <div v-if="isView">
{{ row.operator }} {{ row.operator }}
</div> </div>
<LabeledSelect <Select
v-else v-else
v-model="row.operator" v-model="row.operator"
class="operator single" class="operator single"
@ -253,7 +255,7 @@ export default {
</div> </div>
<input v-else v-model="row.values" :mode="mode" :disabled="row.operator==='Exists' || row.operator==='DoesNotExist'" /> <input v-else v-model="row.values" :mode="mode" :disabled="row.operator==='Exists' || row.operator==='DoesNotExist'" />
</div> </div>
<div class="text-right"> <div class="remove-container">
<button <button
v-if="!isView" v-if="!isView"
type="button" type="button"
@ -273,7 +275,7 @@ export default {
</div> </div>
</template> </template>
<style lang='scss'> <style lang='scss' scoped>
$separator: 20; $separator: 20;
$remove: 75; $remove: 75;
$spacing: 10px; $spacing: 10px;
@ -297,6 +299,11 @@ export default {
font-size:2em; font-size:2em;
} }
.remove-container {
display: flex;
justify-content: center;
}
.selector-weight { .selector-weight {
color: var(--input-label) color: var(--input-label)
} }
@ -305,7 +312,6 @@ export default {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
grid-gap: $column-gutter; grid-gap: $column-gutter;
align-items: center;
&>label{ &>label{
margin-left: 8px; margin-left: 8px;
@ -315,7 +321,7 @@ export default {
grid-template-columns: 1fr 1fr 1fr 100px; grid-template-columns: 1fr 1fr 1fr 100px;
} }
INPUT { INPUT:not(.vs__search) {
height: 50px; height: 50px;
} }
} }

View File

@ -25,7 +25,7 @@ export default {
}, },
extraColumns: { extraColumns: {
type: Array, type: Array,
default: () => [] default: () => [],
}, },
nameLabel: { nameLabel: {
@ -106,31 +106,31 @@ export default {
const metadata = v.metadata; const metadata = v.metadata;
let namespace, name, description; let namespace, name, description;
if (this.nameKey ) { if (this.nameKey) {
name = get(v, this.nameKey); name = get(v, this.nameKey);
} else { } else {
name = metadata.name; name = metadata.name;
} }
if ( this.namespaced ) { if (this.namespaced) {
if ( this.forceNamespace ) { if (this.forceNamespace) {
namespace = this.forceNamespace; namespace = this.forceNamespace;
this.updateNamespace(namespace); this.updateNamespace(namespace);
} else if ( this.namespaceKey ) { } else if (this.namespaceKey) {
namespace = get(v, this.namespaceKey); namespace = get(v, this.namespaceKey);
} else { } else {
namespace = metadata?.namespace; namespace = metadata?.namespace;
} }
if ( !namespace ) { if (!namespace) {
namespace = this.$store.getters['defaultNamespace']; namespace = this.$store.getters['defaultNamespace'];
if ( metadata ) { if (metadata) {
metadata.namespace = namespace; metadata.namespace = namespace;
} }
} }
} }
if ( this.descriptionKey ) { if (this.descriptionKey) {
description = get(v, this.descriptionKey); description = get(v, this.descriptionKey);
} else { } else {
description = metadata?.annotations?.[DESCRIPTION]; description = metadata?.annotations?.[DESCRIPTION];
@ -139,34 +139,39 @@ export default {
return { return {
namespace, namespace,
name, name,
description description,
}; };
}, },
computed: { computed: {
namespaceReallyDisabled() { namespaceReallyDisabled() {
return !!this.forceNamespace || this.namespaceDisabled || this.mode === _EDIT; // namespace is never editable return (
!!this.forceNamespace || this.namespaceDisabled || this.mode === _EDIT
); // namespace is never editable
}, },
nameReallyDisabled() { nameReallyDisabled() {
return this.nameDisabled || ( this.mode === _EDIT && !this.nameEditable); return this.nameDisabled || (this.mode === _EDIT && !this.nameEditable);
}, },
namespaces() { namespaces() {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
const choices = this.$store.getters[`${ inStore }/all`](this.namespaceType); const choices = this.$store.getters[`${ inStore }/all`](this.namespaceType);
const out = sortBy(choices.map((obj) => { const out = sortBy(
return { choices.map((obj) => {
label: obj.nameDisplay, return {
value: obj.id, label: obj.nameDisplay,
}; value: obj.id,
}), 'label'); };
}),
'label'
);
if ( this.forceNamespace ) { if (this.forceNamespace) {
out.unshift({ out.unshift({
label: this.forceNamespace, label: this.forceNamespace,
value: this.forceNamespace value: this.forceNamespace,
}); });
} }
@ -189,7 +194,7 @@ export default {
name(val) { name(val) {
val = val.toLowerCase(); val = val.toLowerCase();
if ( this.nameKey ) { if (this.nameKey) {
set(this.value, this.nameKey, val); set(this.value, this.nameKey, val);
} else { } else {
this.$set(this.value.metadata, 'name', val); this.$set(this.value.metadata, 'name', val);
@ -203,7 +208,7 @@ export default {
}, },
description(val) { description(val) {
if ( this.descriptionKey ) { if (this.descriptionKey) {
set(this.value, this.descriptionKey, val); set(this.value, this.descriptionKey, val);
} else { } else {
this.value.setAnnotation(DESCRIPTION, val); this.value.setAnnotation(DESCRIPTION, val);
@ -222,11 +227,11 @@ export default {
methods: { methods: {
updateNamespace(val) { updateNamespace(val) {
if ( this.forceNamespace ) { if (this.forceNamespace) {
val = this.forceNamespace; val = this.forceNamespace;
} }
if ( this.namespaceKey ) { if (this.namespaceKey) {
set(this.value, this.namespaceKey, val); set(this.value, this.namespaceKey, val);
} else { } else {
this.value.metadata.namespace = val; this.value.metadata.namespace = val;
@ -236,19 +241,20 @@ export default {
changeNameAndNamespace(e) { changeNameAndNamespace(e) {
this.name = (e.text || '').toLowerCase(); this.name = (e.text || '').toLowerCase();
this.namespace = e.selected; this.namespace = e.selected;
} },
} },
}; };
</script> </script>
<template> <template>
<div> <div>
<div class="row mb-20"> <div class="row mb-20">
<div v-show="!nameNsHidden" :class="{col: true, [colSpan]: true}"> <div v-show="!nameNsHidden" :class="{ col: true, [colSpan]: true }">
<slot :namespaces="namespaces" name="namespace"> <slot :namespaces="namespaces" name="namespace">
<InputWithSelect <InputWithSelect
v-if="namespaced" v-if="namespaced"
ref="name" ref="name"
class="namespace-select"
:mode="mode" :mode="mode"
:disabled="namespaceReallyDisabled" :disabled="namespaceReallyDisabled"
:text-label="t(nameLabel)" :text-label="t(nameLabel)"
@ -277,7 +283,7 @@ export default {
/> />
</slot> </slot>
</div> </div>
<div :class="{col: true, [colSpan]: true}"> <div :class="{ col: true, [colSpan]: true }">
<LabeledInput <LabeledInput
key="description" key="description"
v-model="description" v-model="description"
@ -287,10 +293,24 @@ export default {
:min-height="30" :min-height="30"
/> />
</div> </div>
<div v-for="slot in extraColumns" :key="slot" :class="{col: true, [colSpan]: true}"> <div
v-for="slot in extraColumns"
:key="slot"
:class="{ col: true, [colSpan]: true }"
>
<slot :name="slot"> <slot :name="slot">
</slot> </slot>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" scoped>
.row {
.namespace-select ::v-deep {
.labeled-select {
min-width: 40%;
}
}
}
</style>

View File

@ -156,9 +156,9 @@ export default {
v-if="!isView || kind!=='none'" v-if="!isView || kind!=='none'"
v-model="kind" v-model="kind"
:mode="mode" :mode="mode"
:label="t('generic.type')" :label="t('probe.type.label')"
:options="kindOptions" :options="kindOptions"
placeholder="Select a check type" :placeholder="t('probe.type.placeholder')"
/> />
<div v-if="kind && kind!=='none'" class="spacer-small" /> <div v-if="kind && kind!=='none'" class="spacer-small" />
@ -170,8 +170,8 @@ export default {
min="1" min="1"
max="65535" max="65535"
:mode="mode" :mode="mode"
:label="t('workload.container.healthCheck.httpGet.port')" :label="t('probe.httpGet.port.label')"
placeholder="e.g. 80" :placeholder="t('probe.httpGet.port.placeholder')"
/> />
<div class="spacer-small" /> <div class="spacer-small" />
@ -179,8 +179,8 @@ export default {
<LabeledInput <LabeledInput
v-model="httpGet.path" v-model="httpGet.path"
:mode="mode" :mode="mode"
:label="t('workload.container.healthCheck.httpGet.path')" :label="t('probe.httpGet.path.label')"
placeholder="e.g. /healthz" :placeholder="t('probe.httpGet.path.placeholder')"
/> />
</div> </div>
@ -191,8 +191,8 @@ export default {
min="1" min="1"
max="65535" max="65535"
:mode="mode" :mode="mode"
:label="t('workload.container.healthCheck.httpGet.port')" :label="t('probe.httpGet.port.label')"
placeholder="e.g. 25" :placeholder="t('probe.httpGet.port.placeholderDuex')"
/> />
<div class="spacer-small" /> <div class="spacer-small" />
</div> </div>
@ -201,8 +201,8 @@ export default {
<div class="col span-12"> <div class="col span-12">
<ShellInput <ShellInput
v-model="exec.command" v-model="exec.command"
:label="t('workload.container.healthCheck.command.command')" :label="t('probe.httpGet.port.command.label')"
placeholder="e.g. cat /tmp/health" :placeholder="t('probe.httpGet.port.command.placeholder')"
/> />
</div> </div>
<div class="spacer-small" /> <div class="spacer-small" />
@ -219,30 +219,30 @@ export default {
<UnitInput <UnitInput
v-model="probe.periodSeconds" v-model="probe.periodSeconds"
:mode="mode" :mode="mode"
:label="t('workload.container.healthCheck.checkInterval')" :label="t('probe.checkInterval.label')"
min="1" min="1"
:suffix="t('suffix.sec')" :suffix="t('suffix.sec')"
placeholder="Default: 10" :placeholder="t('probe.checkInterval.placeholder')"
/> />
</div> </div>
<div class="col span-4"> <div class="col span-4">
<UnitInput <UnitInput
v-model="probe.initialDelaySeconds" v-model="probe.initialDelaySeconds"
:mode="mode" :mode="mode"
:label="t('workload.container.healthCheck.initialDelay')"
:suffix="t('suffix.sec')" :suffix="t('suffix.sec')"
:label="t('probe.initialDelay.label')"
min="0" min="0"
placeholder="Default: 0" :placeholder="t('probe.initialDelay.placeholder')"
/> />
</div> </div>
<div class="col span-4"> <div class="col span-4">
<UnitInput <UnitInput
v-model="probe.timeoutSeconds" v-model="probe.timeoutSeconds"
:mode="mode" :mode="mode"
:label="t('workload.container.healthCheck.timeout')"
:suffix="t('suffix.sec')" :suffix="t('suffix.sec')"
:label="t('probe.timeout.placeholder')"
min="0" min="0"
placeholder="Default: 3" :placeholder="t('probe.timeout.placeholder')"
/> />
</div> </div>
</div> </div>
@ -256,8 +256,8 @@ export default {
type="number" type="number"
min="1" min="1"
:mode="mode" :mode="mode"
:label="t('workload.container.healthCheck.successThreshold')" :label="t('probe.successThreshold.label')"
placeholder="Default: 1" :placeholder="t('probe.successThreshold.placeholder')"
/> />
</div> </div>
<div class="col span-6"> <div class="col span-6">
@ -266,8 +266,8 @@ export default {
type="number" type="number"
min="1" min="1"
:mode="mode" :mode="mode"
:label="t('workload.container.healthCheck.failureThreshold')" :label="t('probe.failureThreshold.label')"
placeholder="Default: 3" :placeholder="t('probe.failureThreshold.label')"
/> />
</div> </div>
</div> </div>
@ -284,14 +284,14 @@ export default {
:pad-left="false" :pad-left="false"
:as-map="false" :as-map="false"
:read-allowed="false" :read-allowed="false"
:title="t('workload.container.healthCheck.httpGet.headers')" :title="t('probe.httpGet.headers.label')"
:key-label="t('generic.name')" :key-label="t('generic.name')"
:value-label="t('generic.value')" :value-label="t('generic.value')"
:add-label="t('generic.add')" :add-label="t('generic.add')"
> >
<template #title> <template #title>
<h4> <h4>
{{ t('workload.container.healthCheck.httpGet.headers') }} {{ t('probe.httpGet.headers.label') }}
</h4> </h4>
</template> </template>
</KeyValue> </KeyValue>
@ -308,5 +308,8 @@ export default {
.title { .title {
margin-bottom: 10px; margin-bottom: 10px;
} }
::v-deep .labeled-select {
height: auto;
}
</style> </style>

View File

@ -2,7 +2,7 @@
import { _VIEW } from '@/config/query-params'; import { _VIEW } from '@/config/query-params';
import ArrayList from '@/components/form/ArrayList'; import ArrayList from '@/components/form/ArrayList';
import LabeledInput from '@/components/form/LabeledInput'; import LabeledInput from '@/components/form/LabeledInput';
import LabeledSelect from '@/components/form/LabeledSelect'; import Select from '@/components/form/Select';
const OPERATOR_VALUES = { const OPERATOR_VALUES = {
IS_SET: 'Exists', IS_SET: 'Exists',
@ -15,7 +15,7 @@ export default {
components: { components: {
ArrayList, ArrayList,
LabeledInput, LabeledInput,
LabeledSelect Select
}, },
props: { props: {
@ -132,7 +132,7 @@ export default {
<LabeledInput v-model="scope.row.value.key" :mode="mode" /> <LabeledInput v-model="scope.row.value.key" :mode="mode" />
</div> </div>
<div class="operator"> <div class="operator">
<LabeledSelect <Select
:mode="mode" :mode="mode"
:value="scope.row.value.operator" :value="scope.row.value.operator"
:options="operatorOptions" :options="operatorOptions"
@ -147,20 +147,23 @@ export default {
</div> </div>
</template> </template>
<style lang="scss"> <style lang="scss" scoped>
.rule-selector { .rule-selector {
.box { &:not(.view) table {
& > *:not(:last-child) { table-layout: initial;
padding-right: 10px; }
}
.key, .value { ::v-deep .box {
width: 35%; display: grid;
flex: none; grid-template-columns: 25% 25% 25% 15%;
} column-gap: 1.75%;
align-items: center;
margin-bottom: 10px;
.key,
.value,
.operator { .operator {
flex: 1; height: 100%;
} }
} }
} }

View File

@ -1,16 +1,88 @@
<script> <script>
import { createPopper } from '@popperjs/core'; import { createPopper } from '@popperjs/core';
import { get } from '@/utils/object';
import LabeledFormElement from '@/mixins/labeled-form-element';
import VueSelectOverrides from '@/mixins/vue-select-overrides';
export default { export default {
props: { mixins: [LabeledFormElement, VueSelectOverrides],
placement: { props: {
disabled: {
default: false,
type: Boolean,
},
mode: {
default: 'edit',
type: String, type: String,
default: 'bottom' },
optionKey: {
default: null,
type: String,
},
optionLabel: {
default: 'label',
type: String,
},
options: {
default: null,
type: Array,
},
placement: {
default: null,
type: String,
},
placeholder: {
type: String,
default: '',
},
popperOverride: {
type: Function,
default: null,
},
reduce: {
default: (e) => {
if (e && typeof e === 'object' && e.value !== undefined) {
return e.value;
}
return e;
},
type: Function,
},
searchable: {
default: false,
type: Boolean,
},
status: {
type: String,
default: null,
},
value: {
default: null,
type: [String, Object, Number, Array, Boolean],
}, },
}, },
methods: { methods: {
getOptionLabel(option) {
if (this.$attrs['get-option-label']) {
return this.$attrs['get-option-label'](option);
}
if (get(option, this.optionLabel)) {
if (this.localizedLabel) {
return this.$store.getters['i18n/t'](get(option, this.optionLabel));
} else {
return get(option, this.optionLabel);
}
} else {
return option;
}
},
withPopper(dropdownList, component, { width }) { withPopper(dropdownList, component, { width }) {
if (this.popperOverride) {
return this.popperOverride(dropdownList, component, { width });
}
/** /**
* We need to explicitly define the dropdown width since * We need to explicitly define the dropdown width since
* it is usually inherited from the parent with CSS. * it is usually inherited from the parent with CSS.
@ -28,12 +100,11 @@ export default {
* above. * above.
*/ */
const popper = createPopper(component.$refs.toggle, dropdownList, { const popper = createPopper(component.$refs.toggle, dropdownList, {
placement: this.placement, placement: this.placement,
modifiers: [ modifiers: [
{ {
name: 'offset', name: 'offset',
options: { offset: [0, -1] } options: { offset: [0, 2] },
}, },
{ {
name: 'toggleClass', name: 'toggleClass',
@ -43,7 +114,7 @@ export default {
component.$el.setAttribute('x-placement', state.placement); component.$el.setAttribute('x-placement', state.placement);
}, },
}, },
] ],
}); });
/** /**
@ -52,17 +123,56 @@ export default {
*/ */
return () => popper.destroy(); return () => popper.destroy();
}, },
focusSearch() {
this.$nextTick(() => {
this.$refs['select-input'].searchEl.focus();
});
},
get,
}, },
}; };
</script> </script>
<template> <template>
<v-select <div
v-bind="$attrs" class="unlabeled-select"
append-to-body :class="{
:calculate-position="placement ? withPopper : undefined" disabled: disabled && !isView,
v-on="$listeners" focused,
[mode]: true,
[status]: status,
taggable: $attrs.taggable,
}"
@click="focusSearch"
@focus="focusSearch"
> >
<slot /> <v-select
</v-select> v-if="!isView"
ref="select-input"
v-bind="$attrs"
class="inline"
:autoscroll="true"
:append-to-body="!!placement"
:calculate-position="placement ? withPopper : undefined"
:disabled="isView || disabled"
:get-option-key="(opt) => (optionKey ? get(opt, optionKey) : getOptionLabel(opt))"
:get-option-label="(opt) => getOptionLabel(opt)"
:label="optionLabel"
:options="options"
:map-keydown="mappedKeys"
:placeholder="placeholder"
:reduce="(x) => reduce(x)"
:searchable="isSearchable"
:value="value != null ? value : ''"
@input="(e) => $emit('input', e)"
@search:blur="onBlur"
@search:focus="onFocus"
>
<!-- Pass down templates provided by the caller -->
<template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope">
<slot :name="slot" v-bind="scope" />
</template>
</v-select>
</div>
</template> </template>

View File

@ -3,10 +3,10 @@ import debounce from 'lodash/debounce';
import { _EDIT, _VIEW } from '@/config/query-params'; import { _EDIT, _VIEW } from '@/config/query-params';
import { removeAt } from '@/utils/array'; import { removeAt } from '@/utils/array';
import { clone } from '@/utils/object'; import { clone } from '@/utils/object';
import LabeledSelect from '@/components/form/LabeledSelect'; import Select from '@/components/form/Select';
export default { export default {
components: { LabeledSelect }, components: { Select },
props: { props: {
value: { value: {
type: Array, type: Array,
@ -173,7 +173,7 @@ export default {
</div> </div>
<div v-if="showProtocol" class="port-protocol"> <div v-if="showProtocol" class="port-protocol">
<span v-if="isView">{{ row.protocol }}</span> <span v-if="isView">{{ row.protocol }}</span>
<LabeledSelect <Select
v-else v-else
v-model="row.protocol" v-model="row.protocol"
:options="protocolOptions" :options="protocolOptions"
@ -261,8 +261,20 @@ export default {
padding: 0px; padding: 0px;
} }
.ports-row INPUT { .ports-row {
height: 50px; INPUT {
height: 50px;
}
.port-protocol ::v-deep {
.unlabeled-select {
height: 50px;
.v-select.inline {
margin-top: 2px;
}
}
}
} }
.footer { .footer {

View File

@ -1,7 +1,7 @@
<script> <script>
import ArrayList from '@/components/form/ArrayList'; import ArrayList from '@/components/form/ArrayList';
import { _VIEW } from '@/config/query-params'; import { _VIEW } from '@/config/query-params';
import LabeledSelect from '@/components/form/LabeledSelect'; import Select from '@/components/form/Select';
const EFFECT_VALUES = { const EFFECT_VALUES = {
NO_SCHEDULE: 'NoSchedule', NO_SCHEDULE: 'NoSchedule',
@ -10,7 +10,7 @@ const EFFECT_VALUES = {
}; };
export default { export default {
components: { ArrayList, LabeledSelect }, components: { ArrayList, Select },
props: { props: {
value: { value: {
@ -89,7 +89,7 @@ export default {
/> />
</td> </td>
<td class="effect"> <td class="effect">
<LabeledSelect <Select
v-model="scope.row.value.effect" v-model="scope.row.value.effect"
:options="effectOptions" :options="effectOptions"
/> />

View File

@ -204,11 +204,11 @@ export default {
:disabled="isView" :disabled="isView"
:options="routerOptions" :options="routerOptions"
:mode="mode" :mode="mode"
placeholder="Select a Router..." :placeholder="t('target.router.placeholder')"
:clearable="false" :clearable="false"
class="inline" class="inline"
:reduce="opt=>opt.value" :reduce="opt=>opt.value"
label="Router" :label="t('target.router.label')"
@input="update" @input="update"
/> />
</div> </div>
@ -220,11 +220,11 @@ export default {
:disabled="isView" :disabled="isView"
:mode="mode" :mode="mode"
:options="appOptions" :options="appOptions"
placeholder="Select a service" :placeholder="t('target.service.placeholder')"
:reduce="opt=>opt.value" :reduce="opt=>opt.value"
:clearable="false" :clearable="false"
class="inline" class="inline"
label="Service" :label="t('target.service.label')"
@input="update" @input="update"
/> />
<Checkbox v-if="kind==='app'" v-model="pickVersion" label="Target one version" class="mt-10" /> <Checkbox v-if="kind==='app'" v-model="pickVersion" label="Target one version" class="mt-10" />
@ -235,10 +235,10 @@ export default {
:disabled="isView" :disabled="isView"
:mode="mode" :mode="mode"
:options="versionOptions" :options="versionOptions"
placeholder="Select a version" :placeholder="t('target.version.placeholder')"
:clearable="false" :clearable="false"
class="inline" class="inline"
label="Version" :label="t('target.version.label')"
:reduce="opt=>opt.value" :reduce="opt=>opt.value"
@input="update" @input="update"
/> />

View File

@ -1,7 +1,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import LabeledInput from '@/components/form/LabeledInput'; import LabeledInput from '@/components/form/LabeledInput';
import LabeledSelect from '@/components/form/LabeledSelect'; import Select from '@/components/form/Select';
import UnitInput from '@/components/form/UnitInput'; import UnitInput from '@/components/form/UnitInput';
import { _VIEW } from '@/config/query-params'; import { _VIEW } from '@/config/query-params';
import { random32 } from '@/utils/string'; import { random32 } from '@/utils/string';
@ -9,7 +9,7 @@ import { random32 } from '@/utils/string';
export default { export default {
components: { components: {
LabeledInput, LabeledInput,
LabeledSelect, Select,
// SortableTable, // SortableTable,
UnitInput UnitInput
}, },
@ -147,7 +147,7 @@ export default {
<LabeledInput v-model="rule.key" :mode="mode" /> <LabeledInput v-model="rule.key" :mode="mode" />
</div> </div>
<div class="col"> <div class="col">
<LabeledSelect <Select
id="operator" id="operator"
v-model="rule.operator" v-model="rule.operator"
:options="operatorOpts" :options="operatorOpts"
@ -166,7 +166,7 @@ export default {
</div> </div>
</template> </template>
<div class="col"> <div class="col">
<LabeledSelect <Select
v-model="rule.effect" v-model="rule.effect"
:options="effectOpts" :options="effectOpts"
:mode="mode" :mode="mode"
@ -195,24 +195,26 @@ export default {
</template> </template>
<style lang='scss' scoped> <style lang='scss' scoped>
.tolerations{ .tolerations{
width: 100%; width: 100%;
} }
.rule, .toleration-headers{ .rule, .toleration-headers{
display: grid; display: grid;
grid-template-columns: 20% 10% 20% 10% 20% 10%; grid-template-columns: 20% 10% 20% 10% 20% 10%;
grid-gap: $column-gutter; grid-gap: $column-gutter;
align-items: center; align-items: center;
} }
.rule { .rule {
margin-bottom: 20px; margin-bottom: 20px;
} .col {
height: 100%;
.toleration-headers SPAN {
color: var(--input-label);
margin-bottom: 10px;
} }
}
.toleration-headers SPAN {
color: var(--input-label);
margin-bottom: 10px;
}
</style> </style>

View File

@ -258,6 +258,8 @@ export default {
:mode="mode" :mode="mode"
:multiple="false" :multiple="false"
:options="typeOpts" :options="typeOpts"
option-label="label"
:searchable="false"
:reduce="e=>e.value" :reduce="e=>e.value"
:label="t('workload.container.command.fromResource.type')" :label="t('workload.container.command.fromResource.type')"
@input="updateRow" @input="updateRow"

View File

@ -36,7 +36,11 @@ export default {
// show host port column if existing port data has any host ports defined // show host port column if existing port data has any host ports defined
const showHostPorts = !!rows.filter(row => !!row.hostPort).length; const showHostPorts = !!rows.filter(row => !!row.hostPort).length;
return { rows, showHostPorts }; return {
rows,
showHostPorts,
workloadPortOptions: ['TCP', 'UDP']
};
}, },
computed: { computed: {
@ -188,9 +192,8 @@ export default {
<LabeledSelect <LabeledSelect
v-else v-else
v-model="row.protocol" v-model="row.protocol"
:style="{'height':'50px'}"
class="inline" class="inline"
:options="['TCP', 'UDP']" :options="workloadPortOptions"
:multiple="false" :multiple="false"
:label="t('workload.container.ports.protocol')" :label="t('workload.container.ports.protocol')"
@input="queueUpdate" @input="queueUpdate"
@ -248,58 +251,69 @@ export default {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
$remove: 75; $remove: 75;
$checkbox: 75; $checkbox: 75;
.title { .title {
margin-bottom: 10px; margin-bottom: 10px;
.read-from-file { .read-from-file {
float: right; float: right;
}
} }
}
// 1 unit is 8% // 1 unit is 8%
.ports-headers, .ports-row{ .ports-headers, .ports-row{
display: grid; display: grid;
grid-template-columns: 30% 30% 18% 10% 5%; grid-template-columns: 30% 30% 18% 10% 5%;
grid-column-gap: $column-gutter; grid-column-gap: $column-gutter;
margin-bottom: 10px; margin-bottom: 10px;
align-items: center; align-items: center;
& .port{ & .port{
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
}
&.show-host{
grid-template-columns: 30% 16% 8% 16% 16% 5%;
}
} }
.add-host { &.show-host{
justify-self: center; grid-template-columns: 30% 16% 10% 15% 15% 5%;
} }
.ports-headers { }
color: var(--input-label);
}
.toggle-host-ports { .add-host {
color: var(--primary); justify-self: center;
} }
.remove BUTTON { .protocol {
padding: 0px; height: 100%;
} }
.ports-row INPUT { .ports-headers {
height: 50px; color: var(--input-label);
} }
.footer { .toggle-host-ports {
.protip { color: var(--primary);
float: right; }
padding: 5px 0;
} .remove BUTTON {
padding: 0px;
}
.ports-row INPUT {
height: 50px;
}
.footer {
.protip {
float: right;
padding: 5px 0;
} }
}
.ports-row .protocol ::v-deep .unlabeled-select,
.ports-row .protocol ::v-deep .unlabeled-select .v-select {
height: 100%;
}
.ports-row .protocol ::v-deep .unlabeled-select .vs__dropdown-toggle {
padding-top: 12px;
}
</style> </style>

View File

@ -12,18 +12,27 @@ export default {
type: String, type: String,
default: '' default: ''
}, },
addSuffix: { addSuffix: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
addPrefix: {
type: Boolean,
default: true
},
suffix: { suffix: {
type: String, type: String,
default: 'ago', default: 'ago',
}, },
tooltipPlacement: { tooltipPlacement: {
type: String, type: String,
default: 'auto' default: 'auto'
}, },
showTooltip: { showTooltip: {
type: Boolean, type: Boolean,
default: true default: true
@ -84,7 +93,7 @@ export default {
const value = day(this.value); const value = day(this.value);
const now = day(); const now = day();
let diff = value.diff(now, 'seconds'); let diff = value.diff(now, 'seconds');
const prefix = (diff < 0 ? '' : '-'); const prefix = (diff < 0 || !this.addPrefix ? '' : '-');
const suffix = ''; const suffix = '';
diff = Math.abs(diff); diff = Math.abs(diff);

View File

@ -3,17 +3,23 @@ import { MANAGEMENT } from '@/config/types';
import { sortBy } from '@/utils/sort'; import { sortBy } from '@/utils/sort';
import { findBy } from '@/utils/array'; import { findBy } from '@/utils/array';
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import Select from '@/components/form/Select';
export default { export default {
computed: { components: { Select },
computed: {
...mapState(['isMultiCluster']), ...mapState(['isMultiCluster']),
value: { value: {
get() { get() {
const options = this.options; const options = this.options;
const existing = findBy(options, 'id', this.$store.getters['clusterId']); const existing = findBy(
options,
'id',
this.$store.getters['clusterId']
);
if ( existing ) { if (existing) {
return existing; return existing;
} }
@ -22,7 +28,7 @@ export default {
set(neu) { set(neu) {
this.$router.push({ name: 'c-cluster', params: { cluster: neu.id } }); this.$router.push({ name: 'c-cluster', params: { cluster: neu.id } });
} },
}, },
options() { options() {
@ -43,22 +49,20 @@ export default {
methods: { methods: {
focus() { focus() {
this.$refs.select.$refs.search.focus(); this.$refs.select.$refs.search.focus();
} },
}, },
}; };
</script> </script>
<template> <template>
<div class="filter"> <div class="filter">
<v-select <Select
ref="select" ref="select"
key="cluster" key="cluster"
v-model="value" v-model="value"
:selectable="option => option.ready" :selectable="(option) => option.ready"
:clearable="false" :clearable="false"
:options="options" :options="options"
label="label"
> >
<template #selected-option="opt"> <template #selected-option="opt">
<i class="icon icon-copy icon-lg pr-5" /> <i class="icon icon-copy icon-lg pr-5" />
@ -74,60 +78,81 @@ export default {
<template #option="opt"> <template #option="opt">
<b v-if="opt === value">{{ opt.label }}</b> <b v-if="opt === value">{{ opt.label }}</b>
<nuxt-link v-else-if="opt.ready" class="cluster" :to="{name: 'c-cluster', params: { cluster: opt.id }}"> <nuxt-link
v-else-if="opt.ready"
class="cluster"
:to="{ name: 'c-cluster', params: { cluster: opt.id } }"
>
{{ opt.label }} {{ opt.label }}
</nuxt-link> </nuxt-link>
<span v-else class="text-muted">{{ opt.label }}</span> <span v-else class="text-muted">{{ opt.label }}</span>
</template> </template>
</v-select> </Select>
<button v-shortkey.once="['c']" class="hide" @shortkey="focus()" /> <button v-shortkey.once="['c']" class="hide" @shortkey="focus()" />
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.filter ::v-deep .v-select { .filter ::v-deep .unlabeled-select .v-select {
max-width: 100%; background: rgba(0, 0, 0, 0.05);
display: inline-block; border-radius: var(--border-radius);
margin-top: 8px; color: var(--header-btn-text);
display: inline-block;
max-width: 100%;
&.vs--disabled .vs__actions {
display: none;
}
.vs__actions:after {
fill: white !important;
color: white !important;
}
.vs__dropdown-toggle {
background: rgba(0, 0, 0, 0.05);
border-radius: var(--border-radius);
border: 1px solid var(--header-btn-bg); border: 1px solid var(--header-btn-bg);
color: var(--header-btn-text); color: var(--header-btn-text);
background: rgba(0,0,0,.05); height: calc(var(--header-height) - 19px);
border-radius: var(--border-radius); max-width: 100%;
padding-top: 0;
&.vs--disabled .vs__actions { .vs__actions {
display: none; margin-left: -10px;
}
.vs__actions:after {
fill: white !important;
color: white !important;
}
.vs__dropdown-toggle {
height: calc(var(--header-height) - 19px);
background-color: transparent;
border: 0;
.vs__actions {
margin-left: -10px;
}
}
.vs__selected {
user-select: none;
cursor: default;
color: white;
line-height: calc(var(--header-height) - 32px);
} }
} }
.filter ::v-deep INPUT { .vs__selected {
width: auto; border: none;
background-color: transparent; position: absolute;
user-select: none;
cursor: default;
color: white;
line-height: calc(var(--header-height) - 32px);
} }
}
.filter ::v-deep INPUT:hover { .filter ::v-deep .unlabeled-select:not(.view):hover .vs__dropdown-menu {
background-color: transparent; background: var(--dropdown-bg);
} }
.filter ::v-deep .unlabeled-select {
background-color: transparent;
}
.filter ::v-deep .unlabeled-select:not(.focused) {
border: var(--outline-width) solid transparent;
}
.filter ::v-deep .unlabeled-select INPUT[type='search'] {
width: auto;
}
.filter ::v-deep .v-select.inline.vs--single.vs--open .vs__search {
margin-left: 0;
}
.filter ::v-deep .unlabeled-select INPUT:hover {
background-color: transparent;
}
</style> </style>

View File

@ -172,19 +172,20 @@ export default {
} }
> .apps { > .apps {
padding: 0 0 0 5px; padding: 0 5px 0 5px;
} }
> .cluster { > .cluster {
grid-area: cluster; grid-area: cluster;
background-color: var(--header-bg); background-color: var(--header-bg);
position: relative; position: relative;
padding-top: 6px;
} }
> .top { > .top {
grid-area: top; grid-area: top;
background-color: var(--header-bg); background-color: var(--header-bg);
padding-top: 8px; padding-top: 6px;
INPUT[type='search']::placeholder, INPUT[type='search']::placeholder,
.vs__open-indicator, .vs__open-indicator,

View File

@ -3,60 +3,35 @@ import { NAMESPACE_FILTERS } from '@/store/prefs';
import { NAMESPACE, MANAGEMENT } from '@/config/types'; import { NAMESPACE, MANAGEMENT } from '@/config/types';
import { sortBy } from '@/utils/sort'; import { sortBy } from '@/utils/sort';
import { isArray, addObjects, findBy, filterBy } from '@/utils/array'; import { isArray, addObjects, findBy, filterBy } from '@/utils/array';
import Select from '@/components/form/Select';
export default { export default {
components: { Select },
data() {
return {
isHovered: false,
hoveredTimeout: null,
maskedWidth: null,
};
},
computed: { computed: {
value: { filterIsHovered() {
get() { return this.isHovered;
const values = this.$store.getters['prefs/get'](NAMESPACE_FILTERS); },
const options = this.options;
if ( !values ) { maskedDropdownWidth() {
return []; const refs = this.$refs;
} const select = refs.select;
// Remove values that are not valid options if (select) {
const out = values.map((value) => { const selectWidth = select.$el.offsetWidth;
return findBy(options, 'id', value);
}).filter(x => !!x);
return out; return selectWidth;
},
set(neu) {
const old = (this.value || []).slice();
neu = neu.filter(x => !!x.id);
const last = neu[neu.length - 1];
const lastIsSpecial = last?.kind === 'special';
const hadUser = old.find(x => x.id === 'all://user');
const hadAll = old.find(x => x.id === 'all');
if ( lastIsSpecial ) {
neu = [last];
}
if ( neu.length > 1 ) {
neu = neu.filter(x => x.kind !== 'special');
}
if ( neu.find(x => x.id === 'all') ) {
neu = [];
}
let ids;
// If there as something selected and you remove it, go back to user by default
// Unless it was user or all
if (neu.length === 0 && !hadUser && !hadAll ) {
ids = ['all://user'];
} else {
ids = neu.map(x => x.id);
}
this.$store.dispatch('switchNamespaces', ids);
} }
return null;
}, },
options() { options() {
@ -64,63 +39,70 @@ export default {
const out = [ const out = [
{ {
id: 'all', id: 'all',
kind: 'special', kind: 'special',
label: t('nav.ns.all'), label: t('nav.ns.all'),
}, },
{ {
id: 'all://user', id: 'all://user',
kind: 'special', kind: 'special',
label: t('nav.ns.user'), label: t('nav.ns.user'),
}, },
{ {
id: 'all://system', id: 'all://system',
kind: 'special', kind: 'special',
label: t('nav.ns.system'), label: t('nav.ns.system'),
}, },
{ {
id: 'namespaced://true', id: 'namespaced://true',
kind: 'special', kind: 'special',
label: t('nav.ns.namespaced'), label: t('nav.ns.namespaced'),
}, },
{ {
id: 'namespaced://false', id: 'namespaced://false',
kind: 'special', kind: 'special',
label: t('nav.ns.clusterLevel'), label: t('nav.ns.clusterLevel'),
} },
]; ];
divider(); divider();
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
const namespaces = sortBy(this.$store.getters[`${ inStore }/all`](NAMESPACE), ['nameDisplay']); const namespaces = sortBy(
this.$store.getters[`${ inStore }/all`](NAMESPACE),
['nameDisplay']
);
if ( this.$store.getters['isMultiCluster'] ) { if (this.$store.getters['isMultiCluster']) {
const cluster = this.$store.getters['currentCluster']; const cluster = this.$store.getters['currentCluster'];
let projects = this.$store.getters['management/all'](MANAGEMENT.PROJECT); let projects = this.$store.getters['management/all'](
MANAGEMENT.PROJECT
);
projects = sortBy(filterBy(projects, 'spec.clusterName', cluster.id), ['nameDisplay']); projects = sortBy(filterBy(projects, 'spec.clusterName', cluster.id), [
'nameDisplay',
]);
const projectsById = {}; const projectsById = {};
const namespacesByProject = {}; const namespacesByProject = {};
let firstProject = true; let firstProject = true;
namespacesByProject[null] = []; // For namespaces not in a project namespacesByProject[null] = []; // For namespaces not in a project
for ( const project of projects ) { for (const project of projects) {
projectsById[project.metadata.name] = project; projectsById[project.metadata.name] = project;
} }
for (const namespace of namespaces ) { for (const namespace of namespaces) {
let projectId = namespace.projectId; let projectId = namespace.projectId;
if ( !projectId || !projectsById[projectId] ) { if (!projectId || !projectsById[projectId]) {
// If there's a projectId but that project doesn't exist, treat it like no project // If there's a projectId but that project doesn't exist, treat it like no project
projectId = null; projectId = null;
} }
let entry = namespacesByProject[namespace.projectId]; let entry = namespacesByProject[namespace.projectId];
if ( !entry ) { if (!entry) {
entry = []; entry = [];
namespacesByProject[namespace.projectId] = entry; namespacesByProject[namespace.projectId] = entry;
} }
@ -128,10 +110,10 @@ export default {
entry.push(namespace); entry.push(namespace);
} }
for ( const project of projects ) { for (const project of projects) {
const id = project.metadata.name; const id = project.metadata.name;
if ( firstProject ) { if (firstProject) {
firstProject = false; firstProject = false;
} else { } else {
divider(); divider();
@ -140,7 +122,7 @@ export default {
out.push({ out.push({
id: `project://${ id }`, id: `project://${ id }`,
kind: 'project', kind: 'project',
label: t('nav.ns.project', { name: project.nameDisplay }), label: t('nav.ns.project', { name: project.nameDisplay }),
}); });
const forThisProject = namespacesByProject[id] || []; const forThisProject = namespacesByProject[id] || [];
@ -150,8 +132,8 @@ export default {
const orphans = namespacesByProject[null]; const orphans = namespacesByProject[null];
if ( orphans.length ) { if (orphans.length) {
if ( !firstProject ) { if (!firstProject) {
divider(); divider();
} }
@ -171,17 +153,20 @@ export default {
return out; return out;
function addNamespace(namespaces) { function addNamespace(namespaces) {
if ( !isArray(namespaces) ) { if (!isArray(namespaces)) {
namespaces = [namespaces]; namespaces = [namespaces];
} }
addObjects(out, namespaces.map((namespace) => { addObjects(
return { out,
id: `ns://${ namespace.id }`, namespaces.map((namespace) => {
kind: 'namespace', return {
label: t('nav.ns.namespace', { name: namespace.nameDisplay }), id: `ns://${ namespace.id }`,
}; kind: 'namespace',
})); label: t('nav.ns.namespace', { name: namespace.nameDisplay }),
};
})
);
} }
function divider() { function divider() {
@ -191,81 +176,147 @@ export default {
disabled: true, disabled: true,
}); });
} }
} },
value: {
get() {
const values = this.$store.getters['prefs/get'](NAMESPACE_FILTERS);
const options = this.options;
if (!values) {
return [];
}
// Remove values that are not valid options
const out = values
.map((value) => {
return findBy(options, 'id', value);
})
.filter(x => !!x);
return out;
},
set(neu) {
const old = (this.value || []).slice();
neu = neu.filter(x => !!x.id);
const last = neu[neu.length - 1];
const lastIsSpecial = last?.kind === 'special';
const hadUser = old.find(x => x.id === 'all://user');
const hadAll = old.find(x => x.id === 'all');
if (lastIsSpecial) {
neu = [last];
}
if (neu.length > 1) {
neu = neu.filter(x => x.kind !== 'special');
}
if (neu.find(x => x.id === 'all')) {
neu = [];
}
let ids;
// If there as something selected and you remove it, go back to user by default
// Unless it was user or all
if (neu.length === 0 && !hadUser && !hadAll) {
ids = ['all://user'];
} else {
ids = neu.map(x => x.id);
}
this.$store.dispatch('switchNamespaces', ids);
},
},
showGroupedOptions() {
if (this.value && this.value.length >= 2) {
return true;
}
return false;
},
},
mounted() {
this.$nextTick(() => {
this.maskedWidth = this.maskedDropdownWidth;
});
}, },
methods: { methods: {
focus() { focus() {
this.$refs.select.$refs.search.focus(); this.$refs.select.$refs['select-input'].searchEl.focus();
},
focusHandler(event) {
if (event === 'selectBlurred') {
this.maskedWidth = this.maskedDropdownWidth;
this.isHovered = false;
return;
}
// we dont handle blur here because the select specifically handles its blur events and emits when it does.
// we should listen to this blur event because the swapping of the masked select with real select.
if (event.type === 'focus') {
this.isHovered = true;
this.$nextTick(() => {
this.focus();
});
}
},
isHoveredHandler(event) {
clearTimeout(this.hoveredTimeout);
if (event.type === 'mouseenter') {
this.isHovered = true;
} else if (event.type === 'mouseleave') {
this.hoveredTimeout = setTimeout(() => {
this.maskedWidth = this.maskedDropdownWidth;
this.isHovered = false;
}, 200);
}
}, },
}, },
}; };
</script> </script>
<style type="scss" scoped>
.filter ::v-deep .v-select {
min-width: 220px;
max-width: 100%;
display: inline-block;
}
.filter ::v-deep .v-select .vs__selected {
margin: 4px;
user-select: none;
cursor: default;
background: rgba(255, 255, 255, 0.25);
border: solid white thin;
color: white;
height: calc(var(--header-height) - 26px);
}
.filter ::v-deep INPUT {
width: auto;
background-color: transparent;
}
.filter ::v-deep .vs__search::placeholder {
color: white;
}
.filter ::v-deep INPUT:hover {
background-color: transparent;
}
.filter ::v-deep .vs__dropdown-toggle {
max-width: 100%;
border: 1px solid var(--header-btn-bg);
color: var(--header-btn-text);
background: rgba(0, 0, 0, 0.05);
border-radius: var(--border-radius);
height: calc(var(--header-height) - 16px);
}
.filter ::v-deep .vs__deselect:after {
color: white;
}
.filter ::v-deep .v-select .vs__actions:after {
fill: white !important;
color: white !important;
}
.filter ::v-deep INPUT[type='search'] {
padding: 7px;
}
</style>
<template> <template>
<div class="filter"> <div
<v-select class="filter"
:class="{'show-masked': showGroupedOptions && !filterIsHovered}"
@mouseenter="isHoveredHandler"
@mouseleave="isHoveredHandler"
>
<div tabindex="0" class="unlabeled-select masked-dropdown" @focus="focusHandler">
<div class="v-select inline vs--searchable">
<div class="vs__dropdown-toggle">
<div class="vs__selected-options">
<div class="vs__selected">
{{ t('namespaceFilter.selected.label', { total: value.length }) }}
</div>
</div>
<div class="vs__actions"></div>
</div>
</div>
</div>
<Select
ref="select" ref="select"
v-model="value" v-model="value"
:class="{
'has-more': showGroupedOptions,
}"
multiple multiple
:placeholder="t('nav.ns.all')" :placeholder="t('nav.ns.all')"
:selectable="option => !option.disabled && option.id" :selectable="(option) => !option.disabled && option.id"
:options="options" :options="options"
label="label" :close-on-select="false"
@on-blur="focusHandler('selectBlurred')"
> >
<template v-slot:option="opt"> <template v-slot:option="opt">
<template v-if="opt.kind === 'namespace'"> <template v-if="opt.kind === 'namespace'">
@ -281,7 +332,99 @@ export default {
{{ opt.label }} {{ opt.label }}
</template> </template>
</template> </template>
</v-select> </Select>
<button v-shortkey.once="['n']" class="hide" @shortkey="focus()" /> <button v-shortkey.once="['n']" class="hide" @shortkey="focus()" />
</div> </div>
</template> </template>
<style type="scss" scoped>
.filter {
min-width: 220px;
max-width: 100%;
display: inline-block;
}
.filter.show-masked ::v-deep .unlabeled-select:not(.masked-dropdown) {
position: absolute;
left: 0;
top: 0;
height: 0;
opacity: 0;
visibility: hidden;
}
.filter:not(.show-masked) ::v-deep .unlabeled-select.masked-dropdown {
position: absolute;
left: 0;
top: 0;
height: 0;
opacity: 0;
visibility: hidden;
}
.filter ::v-deep .unlabeled-select.has-more .v-select:not(.vs--open) .vs__dropdown-toggle {
overflow: hidden;
}
.filter ::v-deep .unlabeled-select.has-more .v-select.vs--open .vs__dropdown-toggle {
height: max-content;
background-color: var(--header-bg);
}
.filter ::v-deep .unlabeled-select {
background-color: transparent;
}
.filter ::v-deep .unlabeled-select:not(.focused) {
border: var(--outline-width) solid transparent;
}
.filter ::v-deep .unlabeled-select:not(.view):hover .vs__dropdown-menu {
background: var(--dropdown-bg);
}
.filter ::v-deep .unlabeled-select .v-select.inline {
margin: 0;
}
.filter ::v-deep .unlabeled-select .v-select .vs__selected {
margin: 4px;
user-select: none;
cursor: default;
background: rgba(255, 255, 255, 0.25);
border: solid white thin;
color: white;
height: calc(var(--header-height) - 26px);
}
.filter ::v-deep .unlabeled-select .vs__search::placeholder {
color: white;
}
.filter ::v-deep .unlabeled-select INPUT:hover {
background-color: transparent;
}
.filter ::v-deep .unlabeled-select .vs__dropdown-toggle {
background: rgba(0, 0, 0, 0.05);
border-radius: var(--border-radius);
border: 1px solid var(--header-btn-bg);
color: var(--header-btn-text);
height: calc(var(--header-height) - 16px);
max-width: 100%;
padding-top: 0;
}
.filter ::v-deep .unlabeled-select .vs__deselect:after {
color: white;
}
.filter ::v-deep .unlabeled-select .v-select .vs__actions:after {
fill: white !important;
color: white !important;
}
.filter ::v-deep .unlabeled-select INPUT[type='search'] {
padding: 7px;
}
</style>

View File

@ -6,8 +6,11 @@ import { ucFirst } from '@/utils/string';
import { createPopper } from '@popperjs/core'; import { createPopper } from '@popperjs/core';
import $ from 'jquery'; import $ from 'jquery';
import { CATALOG } from '@/config/types'; import { CATALOG } from '@/config/types';
import Select from '@/components/form/Select';
export default { export default {
components: { Select },
data() { data() {
return { previous: null }; return { previous: null };
}, },
@ -162,6 +165,10 @@ export default {
const popper = createPopper(component.$refs.toggle, dropdownList, { const popper = createPopper(component.$refs.toggle, dropdownList, {
strategy: 'fixed', strategy: 'fixed',
modifiers: [ modifiers: [
{
name: 'offset',
options: { offset: [0, -2] },
},
{ {
name: 'toggleClass', name: 'toggleClass',
enabled: true, enabled: true,
@ -181,18 +188,17 @@ export default {
<template> <template>
<div class="filter"> <div class="filter">
<v-select <Select
ref="select" ref="select"
key="product" key="product"
:value="value" :value="value"
:clearable="false" :searchable="false"
:selectable="option => !option.disabled" :selectable="option => !option.disabled"
:options="options" :options="options"
:reduce="opt=>opt.value" :reduce="opt=>opt.value"
:calculate-position="withPopper" :popper-override="withPopper"
:append-to-body="true" :append-to-body="true"
placement="bottom"
label="label"
@input="change" @input="change"
> >
<template v-slot:option="opt"> <template v-slot:option="opt">
@ -205,7 +211,7 @@ export default {
<i v-if="opt.kind === 'external'" class="icon icon-external-link ml-10" /> <i v-if="opt.kind === 'external'" class="icon icon-external-link ml-10" />
</template> </template>
</template> </template>
</v-select> </Select>
<button v-shortkey.once="['p']" class="hide" @shortkey="focus()" /> <button v-shortkey.once="['p']" class="hide" @shortkey="focus()" />
<button v-shortkey.once="['f']" class="hide" @shortkey="switchToFleet()" /> <button v-shortkey.once="['f']" class="hide" @shortkey="switchToFleet()" />
<button v-shortkey.once="['e']" class="hide" @shortkey="switchToExplorer()" /> <button v-shortkey.once="['e']" class="hide" @shortkey="switchToExplorer()" />
@ -214,83 +220,87 @@ export default {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.filter ::v-deep .v-select { .filter ::v-deep .unlabeled-select {
max-width: 100%; background-color: transparent;
display: inline-block; &:not(.focused) {
border: var(--outline-width) solid transparent;
}
.v-select {
&.vs--disabled .vs__actions { &.vs--disabled .vs__actions {
display: none; display: none;
} }
.vs__actions:after { .vs__actions {
fill: white !important; &:after {
color: white !important; fill: white !important;
color: white !important;
}
} }
.vs__dropdown-toggle { .vs__dropdown-toggle {
margin-bottom: -4px;
height: var(--header-height); height: var(--header-height);
// margin-left: 35px;
background-color: transparent; background-color: transparent;
border: 0;
position: relative; position: relative;
// left: 35px; padding-top: 0;
} }
.vs__selected { .vs__selected {
user-select: none; user-select: none;
cursor: default; cursor: default;
color: white; color: white;
line-height: calc(var(--header-height) - 14px); line-height: calc(var(--header-height) - 7px);
position: relative; position: relative;
left: 40px; left: 40px;
align-self: center;
}
.vs__selected-options {
flex-wrap: wrap;
}
.unlabeled-select INPUT[type='search'] {
margin-left: 40px;
} }
} }
}
.filter ::v-deep INPUT {
width: auto;
background-color: transparent;
}
.filter ::v-deep INPUT:hover {
background-color: transparent;
}
</style> </style>
<style lang="scss"> <style lang="scss">
.product-menu { // these styles exist because the dd is placed with Popper and is outside the flow of the component, product-menu gets appended to the menu
width: 300px; .product-menu {
max-height: 90vh; width: 300px;
max-height: 90vh;
&.vs__dropdown-menu {
outline: none;
}
.vs__dropdown-option { .vs__dropdown-option {
padding: 10px; padding: 10px;
text-decoration: none; text-decoration: none;
border-left: 5px solid transparent; border-left: 5px solid transparent;
&.vs__dropdown-option--disabled { &.vs__dropdown-option--disabled {
// The dividers // The dividers
padding: 0; padding: 0;
}
.product-icon {
color: var(--product-icon);
margin-right: 5px;
}
UL {
margin: 0;
}
} }
.vs__dropdown-option--selected { .product-icon {
color: var(--body-text); color: var(--product-icon);
// font-weight: bold; margin-right: 5px;
background: var(--nav-active); }
border-left: 5px solid var(--primary);
.product-icon { UL {
color: var(--product-icon-active); margin: 0;
}
} }
} }
.vs__dropdown-option--selected {
color: var(--body-text);
background: var(--nav-active);
border-left: 5px solid var(--primary);
.product-icon {
color: var(--product-icon-active);
}
}
}
</style> </style>

View File

@ -446,7 +446,7 @@ export default {
v-if="containerChoices.length > 0" v-if="containerChoices.length > 0"
v-model="container" v-model="container"
:disabled="containerChoices.length === 1" :disabled="containerChoices.length === 1"
class="containerPicker auto-width" class="containerPicker"
:options="containerChoices" :options="containerChoices"
:clearable="false" :clearable="false"
placement="top" placement="top"
@ -560,11 +560,11 @@ export default {
} }
} }
.containerPicker ::v-deep .vs__search, .containerPicker {
.range ::v-deep .vs__search { ::v-deep &.unlabeled-select {
width: 0; display: inline-block;
padding: 0; min-width: 200px;
margin: 0; }
opacity: 0;
} }
</style> </style>

View File

@ -268,8 +268,8 @@ export default {
:disabled="containerChoices.length === 1" :disabled="containerChoices.length === 1"
class="containerPicker auto-width" class="containerPicker auto-width"
:options="containerChoices" :options="containerChoices"
:searchable="false"
:clearable="false" :clearable="false"
placement="top"
@input="switchTo($event)" @input="switchTo($event)"
> >
<template #selected-option="option"> <template #selected-option="option">
@ -309,10 +309,10 @@ export default {
} }
} }
.containerPicker ::v-deep .vs__search { .containerPicker {
width: 0; ::v-deep &.unlabeled-select {
padding: 0; display: inline-block;
margin: 0; min-width: 200px;
opacity: 0; }
} }
</style> </style>

View File

@ -17,7 +17,7 @@ export default {
set(value) { set(value) {
this.$store.commit('updateWorkspace', { value }); this.$store.commit('updateWorkspace', { value });
this.$store.dispatch('prefs/set', { key: WORKSPACE, value }); this.$store.dispatch('prefs/set', { key: WORKSPACE, value });
} },
}, },
options() { options() {
@ -29,7 +29,7 @@ export default {
}); });
return out; return out;
} },
}, },
methods: { methods: {
@ -38,48 +38,8 @@ export default {
}, },
}, },
}; };
</script> </script>
<style type="scss" scoped>
.filter ::v-deep .v-select {
max-width: 100%;
display: inline-block;
}
.filter ::v-deep .v-select .vs__selected {
margin: 4px;
user-select: none;
color: white;
height: calc(var(--header-height) - 26px);
}
.filter ::v-deep .vs__dropdown-toggle {
max-width: 100%;
border: 1px solid var(--header-btn-bg);
color: var(--header-btn-text);
background: rgba(0, 0, 0, 0.05);
border-radius: var(--border-radius);
height: calc(var(--header-height) - 16px);
}
.filter ::v-deep .vs__deselect:after {
color: white;
}
.filter ::v-deep .v-select .vs__actions:after {
fill: white !important;
color: white !important;
}
.filter ::v-deep .vs__search {
width: 0;
padding: 0;
margin: 0;
opacity: 0;
}
</style>
<template> <template>
<div class="filter"> <div class="filter">
<Select <Select
@ -88,8 +48,69 @@ export default {
label="label" label="label"
:options="options" :options="options"
:clearable="false" :clearable="false"
:reduce="opt=>opt.value" :reduce="(opt) => opt.value"
/> />
<button v-shortkey.once="['w']" class="hide" @shortkey="focus()" /> <button v-shortkey.once="['w']" class="hide" @shortkey="focus()" />
</div> </div>
</template> </template>
<style type="scss" scoped>
.filter {
min-width: 220px;
max-width: 100%;
display: inline-block;
}
.filter ::v-deep .unlabeled-select {
background-color: transparent;
}
.filter ::v-deep .unlabeled-select:not(.focused) {
border: var(--outline-width) solid transparent;
}
.filter ::v-deep .unlabeled-select:not(.view):hover .vs__dropdown-menu {
background: var(--dropdown-bg);
}
.filter ::v-deep .unlabeled-select .v-select.inline {
margin-top: 0;
}
.filter ::v-deep .unlabeled-select INPUT {
width: auto;
background-color: transparent;
}
.filter ::v-deep .unlabeled-select .vs__search::placeholder {
color: white;
}
.filter ::v-deep .unlabeled-select INPUT:hover {
background-color: transparent;
}
.filter ::v-deep .unlabeled-select .vs__dropdown-toggle {
background: rgba(0, 0, 0, 0.05);
border-radius: var(--border-radius);
border: 1px solid var(--header-btn-bg);
color: var(--header-btn-text);
height: calc(var(--header-height) - 16px);
max-width: 100%;
padding-top: 0;
}
.filter ::v-deep .unlabeled-select .vs__deselect:after {
color: white;
}
.filter ::v-deep .unlabeled-select .v-select .vs__actions:after {
fill: white !important;
color: white !important;
}
.filter ::v-deep .unlabeled-select INPUT[type='search'] {
padding: 7px;
width: auto;
}
</style>

View File

@ -43,29 +43,44 @@ export function init(store) {
sort: ['status.lastRunScanProfileName'], sort: ['status.lastRunScanProfileName'],
}, },
{ {
name: 'total', name: 'total',
label: 'Total', labelKey: 'cis.scan.total',
value: 'status.summary.total', value: 'status.summary.total',
}, },
{ {
name: 'pass', name: 'pass',
label: 'Pass', value: 'status.summary.pass',
value: 'status.summary.pass', labelKey: 'cis.scan.pass',
}, },
{ {
name: 'fail', name: 'fail',
label: 'Fail', labelKey: 'cis.scan.fail',
value: 'status.summary.fail', value: 'status.summary.fail',
}, },
{ {
name: 'skip', name: 'warn',
label: 'Skip', labelKey: 'cis.scan.warn',
value: 'status.summary.skip', value: 'status.summary.warn',
}, },
{ {
name: 'notApplicable', name: 'skip',
label: 'N/A', labelKey: 'cis.scan.skip',
value: 'status.summary.notApplicable', value: 'status.summary.skip',
},
{
name: 'notApplicable',
labelKey: 'cis.scan.notApplicable',
value: 'status.summary.notApplicable',
},
{
name: 'nextScanAt',
label: 'Next Scan',
value: 'status.NextScanAt',
formatter: 'LiveDate',
formatterOpts: { addPrefix: false },
sort: 'status.nextScanAt:desc',
width: 150,
align: 'right',
}, },
{ {
name: 'lastRunTimestamp', name: 'lastRunTimestamp',
@ -79,4 +94,22 @@ export function init(store) {
defaultSort: true, defaultSort: true,
}, },
]); ]);
headers(CIS.CLUSTER_SCAN_PROFILE, [
STATE,
NAME_HEADER,
{
name: 'benchmarkVersion',
labelKey: 'cis.benchmarkVersion',
value: 'spec.benchmarkVersion',
formatter: 'Link',
formatterOpts: { options: { internal: true }, to: { name: 'c-cluster-product-resource-id', params: { resource: CIS.BENCHMARK } } },
},
{
name: 'skippedTests',
labelKey: 'cis.testsSkipped',
value: 'numberTestsSkipped',
sort: ['numberTestsSkipped']
}
]);
} }

View File

@ -2,16 +2,20 @@
import Date from '@/components/formatter/Date'; import Date from '@/components/formatter/Date';
import SortableTable from '@/components/SortableTable'; import SortableTable from '@/components/SortableTable';
import Banner from '@/components/Banner'; import Banner from '@/components/Banner';
import LabeledSelect from '@/components/form/LabeledSelect';
import day from 'dayjs';
import { DATE_FORMAT, TIME_FORMAT } from '@/store/prefs';
import { escapeHtml, randomStr } from '@/utils/string';
import { defaultAsyncData } from '@/components/ResourceDetail'; import { defaultAsyncData } from '@/components/ResourceDetail';
import { CIS } from '@/config/types'; import { CIS } from '@/config/types';
import { STATE } from '@/config/table-headers'; import { STATE } from '@/config/table-headers';
import { randomStr } from '@/utils/string';
export default { export default {
components: { components: {
Date, Date,
SortableTable, SortableTable,
Banner Banner,
LabeledSelect
}, },
props: { props: {
@ -24,7 +28,7 @@ export default {
}, },
async fetch() { async fetch() {
this.clusterReport = await this.value.getReport(); this.clusterReports = await this.value.getReports();
}, },
asyncData(ctx) { asyncData(ctx) {
@ -32,14 +36,12 @@ export default {
}, },
data() { data() {
return { clusterReport: null }; return { clusterReports: [], clusterReport: null };
}, },
computed: { computed: {
parsedReport() { parsedReport() {
const report = this.clusterReport?.parsedReport; return this.clusterReport?.parsedReport || null;
return report;
}, },
reportNodes() { reportNodes() {
@ -96,15 +98,19 @@ export default {
value: this.parsedReport.total value: this.parsedReport.total
}, },
{ {
label: this.t('cis.scan.passed'), label: this.t('cis.scan.pass'),
value: this.parsedReport.pass value: this.parsedReport.pass
}, },
{ {
label: this.t('cis.scan.skipped'), label: this.t('cis.scan.warn'),
value: this.parsedReport.warn
},
{
label: this.t('cis.scan.skip'),
value: this.parsedReport.skip value: this.parsedReport.skip
}, },
{ {
label: this.t('cis.scan.failed'), label: this.t('cis.scan.fail'),
value: this.parsedReport.fail value: this.parsedReport.fail
}, },
{ {
@ -168,14 +174,25 @@ export default {
watch: { watch: {
value(neu) { value(neu) {
try { try {
neu.getReport().then((report) => { neu.getReports().then((reports) => {
this.clusterReport = report; this.clusterReports = reports;
}); });
} catch {} } catch {}
},
clusterReports(neu) {
this.clusterReport = neu[0];
} }
}, },
methods: { methods: {
reportLabel(report = {}) {
const { creationTimestamp } = report.metadata;
const dateFormat = escapeHtml( this.$store.getters['prefs/get'](DATE_FORMAT));
const timeFormat = escapeHtml( this.$store.getters['prefs/get'](TIME_FORMAT));
return day(creationTimestamp).format(`${ dateFormat } ${ timeFormat }`);
},
nodeState(check, node, nodes = []) { nodeState(check, node, nodes = []) {
if (check.state === 'mixed') { if (check.state === 'mixed') {
@ -187,11 +204,13 @@ export default {
testStateSort(state) { testStateSort(state) {
const SORT_ORDER = { const SORT_ORDER = {
fail: 1, other: 7,
skip: 2, notApplicable: 6,
notApplicable: 3, skip: 5,
pass: 4, pass: 4,
other: 5, warn: 3,
mixed: 2,
fail: 1,
}; };
return `${ SORT_ORDER[state] || SORT_ORDER['other'] } ${ state }`; return `${ SORT_ORDER[state] || SORT_ORDER['other'] } ${ state }`;
@ -218,8 +237,17 @@ export default {
<span v-else>{{ item.value }}</span> <span v-else>{{ item.value }}</span>
</div> </div>
</div> </div>
<div v-if="clusterReports.length > 1" class="table-header row mb-20">
<div class="col span-8">
<h3 class="mb-0">
{{ t('cis.scan.scanReport') }}
</h3>
</div>
<div class="col span-4">
<v-select v-model="clusterReport" class="inline" :options="clusterReports" :get-option-label="reportLabel" :get-option-key="report=>report.id" />
</div>
</div>
<div v-if="results.length"> <div v-if="results.length">
<h3>{{ t('cis.scan.scanReport') }}</h3>
<SortableTable <SortableTable
default-sort-by="state" default-sort-by="state"
:search="false" :search="false"
@ -269,4 +297,8 @@ export default {
.sub-table { .sub-table {
padding: 0px 40px 0px 40px; padding: 0px 40px 0px 40px;
} }
.table-header {
align-items: flex-end;
}
</style> </style>

View File

@ -1,17 +1,22 @@
<script> <script>
import CruResource from '@/components/CruResource'; import CruResource from '@/components/CruResource';
import LabeledSelect from '@/components/form/LabeledSelect'; import LabeledSelect from '@/components/form/LabeledSelect';
import LabeledInput from '@/components/form/LabeledInput';
import Banner from '@/components/Banner'; import Banner from '@/components/Banner';
import Loading from '@/components/Loading'; import Loading from '@/components/Loading';
import { CIS, CONFIG_MAP } from '@/config/types'; import { CIS, CONFIG_MAP, ENDPOINTS } from '@/config/types';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import createEditView from '@/mixins/create-edit-view'; import createEditView from '@/mixins/create-edit-view';
import { allHash } from '@/utils/promise'; import { allHash } from '@/utils/promise';
import Checkbox from '@/components/form/Checkbox';
import RadioGroup from '@/components/form/RadioGroup';
import cronstrue from 'cronstrue';
const semver = require('semver'); const semver = require('semver');
export default { export default {
components: { components: {
CruResource, LabeledSelect, Banner, Loading CruResource, LabeledSelect, Banner, Loading, Checkbox, LabeledInput, RadioGroup
}, },
mixins: [createEditView], mixins: [createEditView],
@ -33,9 +38,17 @@ export default {
const hash = await allHash({ const hash = await allHash({
profiles: this.$store.dispatch('cluster/findAll', { type: CIS.CLUSTER_SCAN_PROFILE }), profiles: this.$store.dispatch('cluster/findAll', { type: CIS.CLUSTER_SCAN_PROFILE }),
benchmarks: this.$store.dispatch('cluster/findAll', { type: CIS.BENCHMARK }), benchmarks: this.$store.dispatch('cluster/findAll', { type: CIS.BENCHMARK }),
defaultConfigMap: this.$store.dispatch('cluster/find', { type: CONFIG_MAP, id: 'cis-operator-system/default-clusterscanprofiles' }) defaultConfigMap: this.$store.dispatch('cluster/find', { type: CONFIG_MAP, id: 'cis-operator-system/default-clusterscanprofiles' }),
}); });
try {
await this.$store.dispatch('cluster/find', { type: ENDPOINTS, id: 'cattle-monitoring-system/rancher-monitoring-alertmanager' });
this.hasAlertManager = true;
} catch {
this.hasAlertManager = false;
}
this.allProfiles = hash.profiles; this.allProfiles = hash.profiles;
this.defaultConfigMap = hash.defaultConfigMap; this.defaultConfigMap = hash.defaultConfigMap;
}, },
@ -44,18 +57,31 @@ export default {
if (!this.value.metadata.name) { if (!this.value.metadata.name) {
this.value.metadata.generateName = 'scan-'; this.value.metadata.generateName = 'scan-';
} }
if (!this.value.spec) {
this.value.spec = { scanProfileName: null };
}
return { return {
allProfiles: [], defaultConfigMap: null, scanProfileName: this.value.spec.scanProfileName allProfiles: [], defaultConfigMap: null, scanAlertRule: this.value.spec.scanAlertRule, hasAlertManager: false
}; };
}, },
computed: { computed: {
...mapGetters({ currentCluster: 'currentCluster', t: 'i18n/t' }), ...mapGetters({ currentCluster: 'currentCluster', t: 'i18n/t' }),
cronLabel() {
const { cronSchedule } = this.value.spec;
if (!cronSchedule) {
return null;
}
try {
const hint = cronstrue.toString(cronSchedule);
return hint;
} catch (e) {
return 'invalid cron expression';
}
},
validProfiles() { validProfiles() {
const profileNames = this.allProfiles.filter((profile) => { const profileNames = this.allProfiles.filter((profile) => {
const benchmarkVersion = profile?.spec?.benchmarkVersion; const benchmarkVersion = profile?.spec?.benchmarkVersion;
@ -82,13 +108,20 @@ export default {
} }
return null; return null;
} },
monitoringUrl() {
return this.$router.resolve({
name: 'c-cluster-monitoring',
params: { cluster: this.$route.params.cluster }
}).href;
},
}, },
watch: { watch: {
defaultProfile(neu) { defaultProfile(neu) {
if (neu && !this.scanProfileName) { if (neu && !this.value.spec.scanProfileName) {
this.scanProfileName = neu?.id; this.value.spec.scanProfileName = neu?.id;
} }
}, },
}, },
@ -122,26 +155,56 @@ export default {
<CruResource <CruResource
v-else v-else
:validation-passed="!!scanProfileName" :validation-passed="!!value.spec.scanProfileName"
:done-route="doneRoute" :done-route="doneRoute"
:resource="value" :resource="value"
:mode="mode" :mode="mode"
:errors="errors"
@finish="save" @finish="save"
@error="e=>errors = e"
> >
<template> <template>
<Banner v-if="!validProfiles.length" color="warning" :label="t('cis.noProfiles')" /> <Banner v-if="!validProfiles.length" color="warning" :label="t('cis.noProfiles')" />
<div v-else class="row"> <div v-else class="row mb-20">
<div class="col span-6"> <div class="col span-6">
<LabeledSelect <LabeledSelect
v-model="scanProfileName" v-model="value.spec.scanProfileName"
:mode="mode" :mode="mode"
:label="t('cis.profile')" :label="t('cis.profile')"
:options="validProfiles" :options="validProfiles"
@input="value.spec.scanProfileName = $event"
/> />
</div> </div>
</div> </div>
<h3>Scheduling</h3>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput v-model="value.spec.cronSchedule" :mode="mode" :label="t('cis.cronSchedule.label')" :placeholder="t('cis.cronSchedule.placeholder')" />
<span class="text-muted">{{ cronLabel }}</span>
</div>
<div class="col span-6">
<LabeledInput v-model.number="value.spec.retention" type="number" :mode="mode" :label="t('cis.retention')" />
</div>
</div>
<h3>
Alerting
</h3>
<div class="row mb-20">
<div class="col span-12">
<Banner v-if="scanAlertRule.alertOnFailure || scanAlertRule.alertOnComplete" class="mt-0" :color="hasAlertManager ? 'info' : 'warning'">
<span v-if="!hasAlertManager" v-html="t('cis.alertNotFound')" />
<span v-html="t('cis.alertNeeded', {link: monitoringUrl}, true)" />
</banner>
<Checkbox v-model="scanAlertRule.alertOnComplete" :label="t('cis.alertOnComplete')" />
<Checkbox v-model="scanAlertRule.alertOnFailure" :label="t('cis.alertOnFailure')" />
</div>
</div>
<div class="row">
<div class="col">
<span>{{ t('cis.scoreWarning.label') }}</span> <i v-tooltip="t('cis.scoreWarning.protip')" class="icon icon-info" />
<RadioGroup v-model="value.spec.scoreWarning" name="scoreWarning" :options="['pass', 'fail']" :labels="[t('cis.scan.pass'), t('cis.scan.fail')]" />
</div>
</div>
</template> </template>
</CruResource> </CruResource>
</template> </template>

View File

@ -98,7 +98,15 @@ export default {
<template> <template>
<div> <div>
<CruResource :validation-passed="!!name && !!value.spec.benchmarkVersion" :done-route="doneRoute" :resource="value" :mode="mode" @finish="save"> <CruResource
:validation-passed="!!name && !!value.spec.benchmarkVersion"
done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
@finish="save"
@error="e=>errors = e"
>
<template> <template>
<div class="spacer"></div> <div class="spacer"></div>
<div class="row"> <div class="row">

View File

@ -50,6 +50,11 @@ export default {
return { return {
selectedSeverityLabel: null, selectedSeverityLabel: null,
ignoredAnnotations: INGORED_ANNOTATIONS, ignoredAnnotations: INGORED_ANNOTATIONS,
severityOptions: [
this.t('prometheusRule.alertingRules.labels.severity.choices.critical'),
this.t('prometheusRule.alertingRules.labels.severity.choices.warning'),
this.t('prometheusRule.alertingRules.labels.severity.choices.none'),
],
}; };
}, },
@ -336,11 +341,7 @@ export default {
:mode="mode" :mode="mode"
:label="t('prometheusRule.alertingRules.labels.severity.choices.label')" :label="t('prometheusRule.alertingRules.labels.severity.choices.label')"
:localized-label="false" :localized-label="false"
:options="[ :options="severityOptions"
t('prometheusRule.alertingRules.labels.severity.choices.critical'),
t('prometheusRule.alertingRules.labels.severity.choices.warning'),
t('prometheusRule.alertingRules.labels.severity.choices.none'),
]"
/> />
</div> </div>
</div> </div>

View File

@ -67,13 +67,13 @@ export default {
</div> </div>
</div> </div>
<Banner v-if="value.isRoot" color="info"> <Banner v-if="value.isRoot" color="info">
This is the top-level Route used by Alertmanager as the default destination for any Alerts that do not match any other Routes. This Route must exist and cannot be deleted. {{ t("monitoringRoute.info") }}
</Banner> </Banner>
<Tabbed ref="tabbed" :side-tabs="true" default-tab="overview"> <Tabbed ref="tabbed" :side-tabs="true" default-tab="overview">
<Tab label="Receiver" :weight="2" name="receiver"> <Tab label="Receiver" :weight="2" name="receiver">
<div class="row"> <div class="row">
<div class="col span-6"> <div class="col span-6">
<LabeledSelect v-model="value.spec.receiver" :options="receiverOptions" label="Receiver" :mode="mode" /> <LabeledSelect v-model="value.spec.receiver" :options="receiverOptions" :label="t('monitoringRoute.receiver.label')" :mode="mode" />
</div> </div>
</div> </div>
</Tab> </Tab>
@ -81,9 +81,9 @@ export default {
<div class="row mb-20"> <div class="row mb-20">
<div class="col span-6"> <div class="col span-6">
<span class="label"> <span class="label">
Group By: {{ t("monitoringRoute.groups.label") }}:
</span> </span>
<ArrayList v-if="!isView || value.spec.group_by.length > 0" v-model="value.spec.group_by" label="Group By" :mode="mode" :initial-empty-row="true" /> <ArrayList v-if="!isView || value.spec.group_by.length > 0" v-model="value.spec.group_by" :label="t('monitoringRoute.groups.label')" :mode="mode" :initial-empty-row="true" />
<div v-else> <div v-else>
{{ t('generic.none') }} {{ t('generic.none') }}
</div> </div>
@ -92,32 +92,33 @@ export default {
<hr class="divider" /> <hr class="divider" />
<div class="row mb-10"> <div class="row mb-10">
<div class="col span-6"> <div class="col span-6">
<LabeledInput v-model="value.spec.group_wait" label="Group Wait" :mode="mode" /> <LabeledInput v-model="value.spec.group_wait" :label="t('monitoringRoute.wait.label')" :mode="mode" />
</div> </div>
<div class="col span-6"> <div class="col span-6">
<LabeledInput v-model="value.spec.group_interval" label="Group Interval" :mode="mode" /> <LabeledInput v-model="value.spec.group_interval" :label="t('monitoringRoute.interval.label')" :mode="mode" />
</div> </div>
</div> </div>
<div class="row mb-10"> <div class="row mb-10">
<div class="col span-6"> <div class="col span-6">
<LabeledInput v-model="value.spec.repeat_interval" label="Repeat Interval" :mode="mode" /> <LabeledInput v-model="value.spec.repeat_interval" :label="t('monitoringRoute.repeatInterval.label')" :mode="mode" />
</div> </div>
</div> </div>
</Tab> </Tab>
<Tab label="Matching" :weight="1" name="matching"> <Tab label="Matching" :weight="1" name="matching">
<Banner v-if="value.isRoot" color="info"> <Banner v-if="value.isRoot" color="info">
The root route has to match everything so matching can't be configured. {{ t('monitoringRoute.matching.info') }}
</Banner> </Banner>
<div v-else class="row"> <div v-else class="row">
<div class="col span-12"> <div class="col span-12">
<span class="label"> <span class="label">
Match: {{ t('monitoringRoute.matching.label') }}
</span> </span>
<KeyValue <KeyValue
v-if="!isView || Object.keys(value.spec.match || {}).length > 0" v-if="!isView || Object.keys(value.spec.match || {}).length > 0"
v-model="value.spec.match" v-model="value.spec.match"
:disabled="value.isRoot" :disabled="value.isRoot"
:options="receiverOptions" :options="receiverOptions"
:label="t('monitoringRoute.receiver.label')"
:mode="mode" :mode="mode"
:read-allowed="false" :read-allowed="false"
add-label="Add match" add-label="Add match"
@ -130,13 +131,14 @@ export default {
<div class="row mt-40"> <div class="row mt-40">
<div class="col span-12"> <div class="col span-12">
<span class="label"> <span class="label">
Match Regex: {{ t('monitoringRoute.regex.label') }}:
</span> </span>
<KeyValue <KeyValue
v-if="!isView || Object.keys(value.spec.match_re || {}).length > 0" v-if="!isView || Object.keys(value.spec.match_re || {}).length > 0"
v-model="value.spec.match_re" v-model="value.spec.match_re"
:disabled="value.isRoot" :disabled="value.isRoot"
:options="receiverOptions" :options="receiverOptions"
:label="t('monitoringRoute.receiver.label')"
:mode="mode" :mode="mode"
:read-allowed="false" :read-allowed="false"
add-label="Add match regex" add-label="Add match regex"

View File

@ -102,7 +102,7 @@ export default {
:extra-columns="extraColumns" :extra-columns="extraColumns"
> >
<template #project-col> <template #project-col>
<LabeledSelect v-model="project" label="Project" :options="projectOpts" /> <LabeledSelect v-model="project" :label="t('namespace.project.label')" :options="projectOpts" />
</template> </template>
</NameNsDescription> </NameNsDescription>

View File

@ -1,12 +1,13 @@
<script> <script>
import InputWithSelect from '@/components/form/InputWithSelect'; import InputWithSelect from '@/components/form/InputWithSelect';
import LabeledInput from '@/components/form/LabeledInput'; import LabeledInput from '@/components/form/LabeledInput';
import LabeledSelect from '@/components/form/LabeledSelect'; import Select from '@/components/form/Select';
import { get, set } from '@/utils/object'; import { get, set } from '@/utils/object';
import debounce from 'lodash/debounce';
export default { export default {
components: { components: {
InputWithSelect, LabeledInput, LabeledSelect InputWithSelect, LabeledInput, Select
}, },
props: { props: {
value: { value: {
@ -53,6 +54,10 @@ export default {
return this.serviceTargetStatus === 'warning' ? this.t('ingress.rules.target.doesntExist') : null; return this.serviceTargetStatus === 'warning' ? this.t('ingress.rules.target.doesntExist') : null;
}, },
}, },
created() {
this.queueUpdate = debounce(this.update, 500);
this.queueUpdatePathTypeAndPath = debounce(this.updatePathTypeAndPath, 500);
},
methods: { methods: {
update() { update() {
const servicePort = Number.parseInt(this.servicePort) || this.servicePort; const servicePort = Number.parseInt(this.servicePort) || this.servicePort;
@ -82,14 +87,14 @@ export default {
:placeholder="t('ingress.rules.path.placeholder', undefined, true)" :placeholder="t('ingress.rules.path.placeholder', undefined, true)"
:select-value="pathType" :select-value="pathType"
:text-value="path" :text-value="path"
@input="updatePathTypeAndPath" @input="queueUpdatePathTypeAndPath"
/> />
</div> </div>
<div v-else class="col span-4"> <div v-else class="col span-4">
<input v-model="path" :placeholder="t('ingress.rules.path.placeholder', undefined, true)" @input="update" /> <input v-model="path" :placeholder="t('ingress.rules.path.placeholder', undefined, true)" @input="queueUpdate" />
</div> </div>
<div class="col" :class="{'span-3': ingress.showPathType, 'span-4': !ingress.showPathType}"> <div class="col" :class="{'span-3': ingress.showPathType, 'span-4': !ingress.showPathType}">
<LabeledSelect <Select
v-model="serviceName" v-model="serviceName"
option-label="label" option-label="label"
option-key="label" option-key="label"
@ -98,7 +103,7 @@ export default {
:taggable="true" :taggable="true"
:tooltip="serviceTargetTooltip" :tooltip="serviceTargetTooltip"
:hover-tooltip="true" :hover-tooltip="true"
@input="update(); servicePort = ''" @input="queueUpdate(); servicePort = ''"
/> />
</div> </div>
<div class="col" :class="{'span-2': ingress.showPathType, 'span-3': !ingress.showPathType}" :style="{'margin-right': '0px'}"> <div class="col" :class="{'span-2': ingress.showPathType, 'span-3': !ingress.showPathType}" :style="{'margin-right': '0px'}">
@ -106,14 +111,14 @@ export default {
v-if="portOptions.length === 0" v-if="portOptions.length === 0"
v-model="servicePort" v-model="servicePort"
:placeholder="t('ingress.rules.port.placeholder')" :placeholder="t('ingress.rules.port.placeholder')"
@input="update" @input="queueUpdate"
/> />
<LabeledSelect <Select
v-else v-else
v-model="servicePort" v-model="servicePort"
:options="portOptions" :options="portOptions"
:placeholder="t('ingress.rules.port.placeholder')" :placeholder="t('ingress.rules.port.placeholder')"
@input="update" @input="queueUpdate"
/> />
</div> </div>
<button class="btn btn-sm role-link col" @click="$emit('remove')"> <button class="btn btn-sm role-link col" @click="$emit('remove')">
@ -122,12 +127,16 @@ export default {
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.rule-path { .rule-path ::v-deep {
button { button {
line-height: 40px; line-height: 40px;
} }
input {
height: 55px; .v-select INPUT {
height: 50px;
}
.labeled-input {
padding-top: 6px;
} }
} }
</style> </style>

View File

@ -129,6 +129,7 @@ export default {
<Certificates v-model="value" :mode="mode" :secrets="allSecrets" /> <Certificates v-model="value" :mode="mode" :secrets="allSecrets" />
</Tab> </Tab>
<Tab <Tab
v-if="!isView"
name="labels-and-annotations" name="labels-and-annotations"
:label="t('generic.labelsAndAnnotations')" :label="t('generic.labelsAndAnnotations')"
:weight="0" :weight="0"

View File

@ -2,8 +2,10 @@
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import { RIO } from '@/config/types'; import { RIO } from '@/config/types';
import { _VIEW } from '@/config/query-params'; import { _VIEW } from '@/config/query-params';
import Select from '@/components/form/Select';
export default { export default {
components: { Select },
props: { props: {
mode: { mode: {
type: String, type: String,
@ -122,7 +124,7 @@ export default {
<template> <template>
<tr> <tr>
<td> <td>
<v-select <Select
class="inline" class="inline"
:clearable="false" :clearable="false"
:value="app" :value="app"
@ -130,10 +132,10 @@ export default {
:placeholder="showPlaceholders ? placeholders[0] : null" :placeholder="showPlaceholders ? placeholders[0] : null"
:disabled="isView" :disabled="isView"
@input="setApp" @input="setApp"
></v-select> />
</td> </td>
<td v-if="pickVersion"> <td v-if="pickVersion">
<v-select <Select
v-model="version" v-model="version"
class="inline" class="inline"
:options="versions" :options="versions"

View File

@ -11,8 +11,9 @@
*/ */
import LabeledInput from '@/components/form/LabeledInput'; import LabeledInput from '@/components/form/LabeledInput';
import Select from '@/components/form/Select';
export default { export default {
components: { LabeledInput }, components: { LabeledInput, Select },
props: { props: {
spec: { spec: {
type: Object, type: Object,
@ -38,7 +39,10 @@ export default {
}); });
} }
return { all }; return {
all,
ruleOperatorOptions: ['add', 'set', 'remove']
};
}, },
methods: { methods: {
format() { format() {
@ -84,7 +88,7 @@ export default {
</tr> </tr>
<tr v-for="(rule, i) in all" :key="i"> <tr v-for="(rule, i) in all" :key="i">
<td> <td>
<v-select v-model="rule.op" :disabled="!enabled" :serachable="false" class="inline" :options="['add', 'set', 'remove']" /> <Select v-model="rule.op" :disabled="!enabled" :serachable="false" class="inline" :options="ruleOperatorOptions" />
</td> </td>
<td> <td>
<LabeledInput v-model="rule.name" /> <LabeledInput v-model="rule.name" />

View File

@ -3,12 +3,14 @@ import debounce from 'lodash/debounce';
import { _EDIT, _VIEW } from '@/config/query-params'; import { _EDIT, _VIEW } from '@/config/query-params';
import { removeAt, sameContents } from '@/utils/array'; import { removeAt, sameContents } from '@/utils/array';
import { clone } from '@/utils/object'; import { clone } from '@/utils/object';
import Select from '@/components/form/Select';
const READ_VERBS = ['get', 'list', 'watch']; const READ_VERBS = ['get', 'list', 'watch'];
const WRITE_VERBS = ['create', 'delete', 'get', 'list', 'patch', 'update', 'watch']; const WRITE_VERBS = ['create', 'delete', 'get', 'list', 'patch', 'update', 'watch'];
export default { export default {
props: { components: { Select },
props: {
value: { value: {
type: Array, type: Array,
default: null, default: null,
@ -160,7 +162,7 @@ export default {
<template v-else> <template v-else>
<td class="verbs"> <td class="verbs">
<span v-if="isView">{{ row.verbs.join(',') }}</span> <span v-if="isView">{{ row.verbs.join(',') }}</span>
<v-select <Select
v-else v-else
ref="verbs" ref="verbs"
v-model="row.verbs" v-model="row.verbs"

View File

@ -262,7 +262,7 @@ export default {
<div class="spacer"></div> <div class="spacer"></div>
<ResourceTabs v-if="!isView" v-model="value" :side-tabs="true" :mode="mode"> <ResourceTabs v-if="!isView" v-model="value" :side-tabs="true" :mode="mode">
<Tab name="data" label="Data"> <Tab name="data" :label="t('secret.data')">
<template v-if="isRegistry"> <template v-if="isRegistry">
<div id="registry-type" class="row mb-10"> <div id="registry-type" class="row mb-10">
<div class="col span-12"> <div class="col span-12">
@ -348,7 +348,7 @@ export default {
key="data" key="data"
v-model="value.data" v-model="value.data"
:mode="mode" :mode="mode"
title="Data" :title="t('secret.data')"
:initial-empty-row="true" :initial-empty-row="true"
:value-base64="true" :value-base64="true"
:file-modifier="fileModifier" :file-modifier="fileModifier"

View File

@ -142,15 +142,16 @@ export default {
} }
return { return {
name: this.value?.metadata?.name || null, allConfigMaps: [],
allNodes: null,
allSecrets: [],
allServices: [],
name: this.value?.metadata?.name || null,
pvcs: [],
showTabs: false,
pullPolicyOptions: ['Always', 'IfNotPresent', 'Never'],
spec, spec,
type, type,
allConfigMaps: [],
allSecrets: [],
allServices: [],
pvcs: [],
allNodes: null,
showTabs: false,
}; };
}, },
@ -640,7 +641,7 @@ export default {
<LabeledSelect <LabeledSelect
v-model="container.imagePullPolicy" v-model="container.imagePullPolicy"
:label="t('workload.container.imagePullPolicy')" :label="t('workload.container.imagePullPolicy')"
:options="['Always', 'IfNotPresent', 'Never']" :options="pullPolicyOptions"
:mode="mode" :mode="mode"
/> />
</div> </div>

View File

@ -47,7 +47,7 @@ export default {
<div> <div>
<div class="row mb-10"> <div class="row mb-10">
<div class="col span-6"> <div class="col span-6">
<LabeledSelect v-model="value.csi.driver" :mode="mode" label="Driver" :options="driverOpts" :get-option-label="opt=>t(`workload.storage.csi.drivers.'${opt}'`)" /> <LabeledSelect v-model="value.csi.driver" :mode="mode" :label="t('workload.storage.driver')" :options="driverOpts" :get-option-label="opt=>t(`workload.storage.csi.drivers.'${opt}'`)" />
</div> </div>
</div> </div>
<div v-if="driverComponent" class="mb-10"> <div v-if="driverComponent" class="mb-10">

View File

@ -55,7 +55,7 @@ export default {
<h3>{{ t('workload.storage.subtypes.csi') }}</h3> <h3>{{ t('workload.storage.subtypes.csi') }}</h3>
<div class="row mb-10"> <div class="row mb-10">
<div class="col span-6"> <div class="col span-6">
<LabeledSelect v-model="value.csi.driver" :mode="mode" label="Driver" :options="driverOpts" :get-option-label="opt=>t(`workload.storage.csi.drivers.'${opt}'`)" /> <LabeledSelect v-model="value.csi.driver" :mode="mode" :label="t('workload.storage.driver')" :options="driverOpts" :get-option-label="opt=>t(`workload.storage.csi.drivers.'${opt}'`)" />
</div> </div>
</div> </div>
<div v-if="value.csi.driver && driverComponent" class="mb-10"> <div v-if="value.csi.driver && driverComponent" class="mb-10">

View File

@ -13,10 +13,10 @@ export default {
ButtonDropdown, Mount, CodeMirror, InfoBox ButtonDropdown, Mount, CodeMirror, InfoBox
}, },
props: { props: {
mode: { mode: {
type: String, type: String,
default: 'create' default: 'create',
}, },
// pod spec // pod spec
@ -24,23 +24,23 @@ export default {
type: Object, type: Object,
default: () => { default: () => {
return {}; return {};
} },
}, },
namespace: { namespace: {
type: String, type: String,
default: null default: null,
}, },
// namespaced configmaps and secrets // namespaced configmaps and secrets
configMaps: { configMaps: {
type: Array, type: Array,
default: () => [] default: () => [],
}, },
secrets: { secrets: {
type: Array, type: Array,
default: () => [] default: () => [],
}, },
registerBeforeHook: { registerBeforeHook: {
@ -66,17 +66,53 @@ export default {
}, },
opts() { opts() {
const hasComponent = require.context('@/edit/workload/storage', false, /^.*\.vue$/).keys() const hasComponent = require
.context('@/edit/workload/storage', false, /^.*\.vue$/)
.keys()
.map(path => path.replace(/(\.\/)|(.vue)/g, '')) .map(path => path.replace(/(\.\/)|(.vue)/g, ''))
.filter(file => file !== 'index' && file !== 'Mount' && file !== 'PVC'); .filter(
file => file !== 'index' && file !== 'Mount' && file !== 'PVC'
);
const out = [...hasComponent, 'csi', 'configMap', 'createPVC', 'persistentVolumeClaim']; const out = [
...hasComponent,
'csi',
'configMap',
'createPVC',
'persistentVolumeClaim',
];
out.sort(); out.sort();
return out; return out;
}, },
opts2() {
const hasComponent = require
.context('@/edit/workload/storage', false, /^.*\.vue$/)
.keys()
.map(path => path.replace(/(\.\/)|(.vue)/g, ''))
.filter(
file => file !== 'index' && file !== 'Mount' && file !== 'PVC'
);
const out = [
...hasComponent,
'csi',
'configMap',
'createPVC',
'persistentVolumeClaim',
];
out.sort();
return out.map(opt => ({
label: opt,
action: this.addVolume,
value: opt,
}));
},
pvcNames() { pvcNames() {
return this.pvcs.map(pvc => pvc.metadata.name); return this.pvcs.map(pvc => pvc.metadata.name);
}, },
@ -97,15 +133,21 @@ export default {
addVolume(type) { addVolume(type) {
if (type === 'createPVC') { if (type === 'createPVC') {
this.value.volumes.push({ this.value.volumes.push({
_type: 'createPVC', persistentVolumeClaim: {}, name: `vol${ this.value.volumes.length }` _type: 'createPVC',
persistentVolumeClaim: {},
name: `vol${ this.value.volumes.length }`,
}); });
} else if ( type === 'csi' ) { } else if (type === 'csi') {
this.value.volumes.push({ this.value.volumes.push({
_type: type, csi: { volumeAttributes: {} }, name: `vol${ this.value.volumes.length }` _type: type,
csi: { volumeAttributes: {} },
name: `vol${ this.value.volumes.length }`,
}); });
} else { } else {
this.value.volumes.push({ this.value.volumes.push({
_type: type, [type]: {}, name: `vol${ this.value.volumes.length }` _type: type,
[type]: {},
name: `vol${ this.value.volumes.length }`,
}); });
} }
}, },
@ -115,7 +157,9 @@ export default {
}, },
volumeType(vol) { volumeType(vol) {
const type = Object.keys(vol).filter(key => typeof vol[key] === 'object')[0]; const type = Object.keys(vol).filter(
key => typeof vol[key] === 'object'
)[0];
return type; return type;
}, },
@ -127,7 +171,8 @@ export default {
return require(`@/edit/workload/storage/secret.vue`).default; return require(`@/edit/workload/storage/secret.vue`).default;
case 'createPVC': case 'createPVC':
case 'persistentVolumeClaim': case 'persistentVolumeClaim':
return require(`@/edit/workload/storage/persistentVolumeClaim/index.vue`).default; return require(`@/edit/workload/storage/persistentVolumeClaim/index.vue`)
.default;
case 'csi': case 'csi':
return require(`@/edit/workload/storage/csi/index.vue`).default; return require(`@/edit/workload/storage/csi/index.vue`).default;
default: { default: {
@ -135,8 +180,7 @@ export default {
try { try {
component = require(`@/edit/workload/storage/${ type }.vue`).default; component = require(`@/edit/workload/storage/${ type }.vue`).default;
} catch { } catch {}
}
return component; return component;
} }
@ -144,7 +188,9 @@ export default {
}, },
headerFor(type) { headerFor(type) {
if (this.$store.getters['i18n/exists'](`workload.storage.subtypes.${ type }`)) { if (
this.$store.getters['i18n/exists'](`workload.storage.subtypes.${ type }`)
) {
return this.t(`workload.storage.subtypes.${ type }`); return this.t(`workload.storage.subtypes.${ type }`);
} else { } else {
return type; return type;
@ -164,17 +210,16 @@ export default {
try { try {
button.togglePopover(); button.togglePopover();
} catch (e) { } catch (e) {}
}
}, },
// codemirror needs to refresh if it is in a tab that wasn't visible on page load // codemirror needs to refresh if it is in a tab that wasn't visible on page load
refresh() { refresh() {
if ( this.$refs.cm ) { if (this.$refs.cm) {
this.$refs.cm.forEach(component => component.refresh()); this.$refs.cm.forEach(component => component.refresh());
} }
}, },
} },
}; };
</script> </script>
@ -203,7 +248,7 @@ export default {
<CodeMirror <CodeMirror
ref="cm" ref="cm"
:value="yamlDisplay(volume)" :value="yamlDisplay(volume)"
:options="{readOnly:true, cursorBlinkRate:-1}" :options="{ readOnly: true, cursorBlinkRate: -1 }"
/> />
</div> </div>
</div> </div>
@ -212,30 +257,24 @@ export default {
</div> </div>
<div class="row"> <div class="row">
<div class="col span-6"> <div class="col span-6">
<ButtonDropdown v-if="mode!=='view'" ref="buttonDropdown" size="sm"> <ButtonDropdown
<template #button-content> :button-label="t('workload.storage.addVolume')"
<button v-if="mode!=='view'" type="button" class="btn btn-sm text-primary bg-transparent" @click="openPopover"> :dropdown-options="opts"
{{ t('workload.storage.addVolume') }} size="sm"
</button> @click-action="addVolume"
</template> />
<template #popover-content>
<ul class="list-unstyled menu">
<li v-for="opt in opts" :key="opt" v-close-popover @click="addVolume(opt)">
{{ t(`workload.storage.subtypes.${opt}`) }}
</li>
</ul>
</template>
</ButtonDropdown>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style lang='scss' scoped> <style lang='scss' scoped>
.volume-source{ .volume-source {
padding: 20px;
margin: 20px 0px 20px 0px;
position: relative; position: relative;
::v-deep .code-mirror { ::v-deep .code-mirror {
.CodeMirror { .CodeMirror {
background-color: var(--yaml-editor-bg); background-color: var(--yaml-editor-bg);
& .CodeMirror-gutters { & .CodeMirror-gutters {
@ -249,12 +288,11 @@ export default {
position: absolute; position: absolute;
top: 10px; top: 10px;
right: 10px; right: 10px;
padding:0px; padding: 0px;
} }
.add-vol:focus{ .add-vol:focus {
outline: none; outline: none;
box-shadow: none; box-shadow: none;
} }
</style> </style>

View File

@ -54,10 +54,23 @@ export default {
notView() { notView() {
return (this.mode !== _VIEW); return (this.mode !== _VIEW);
}, },
isSearchable() {
const { searchable } = this;
const options = ( this.options || [] );
if (searchable || options.length >= 10) {
return true;
}
return false;
},
}, },
methods: { methods: {
onFocus() { onFocus() {
this.$emit('on-focus');
return this.onFocusLabeled(); return this.onFocusLabeled();
}, },
@ -67,6 +80,8 @@ export default {
}, },
onBlur() { onBlur() {
this.$emit('on-blur');
return this.onBlurLabeled(); return this.onBlurLabeled();
}, },

View File

@ -0,0 +1,37 @@
import $ from 'jquery';
export default {
methods: {
mappedKeys(map, vm) {
// Defaults found at - https://github.com/sagalbot/vue-select/blob/master/src/components/Select.vue#L947
const out = { ...map };
// tab
(out[9] = (e) => {
e.preventDefault();
const optsLen = vm.filteredOptions.length;
const typeAheadPointer = vm.typeAheadPointer;
if (e.shiftKey) {
if (typeAheadPointer === 0) {
vm.$refs.search.focus();
return vm.onEscape();
}
return vm.typeAheadUp();
}
if (typeAheadPointer + 1 === optsLen) {
$(vm.$el).next().focus();
return vm.onEscape();
}
return vm.typeAheadDown();
});
return out;
},
}
};

View File

@ -1,9 +1,11 @@
import { CIS } from '@/config/types'; import { CIS } from '@/config/types';
import { downloadFile } from '@/utils/download'; import { findBy } from '@/utils/array';
import { downloadFile, generateZip } from '@/utils/download';
import { isEmpty, set } from '@/utils/object';
import { sortBy } from '@/utils/sort';
export default { export default {
_availableActions() { _availableActions() {
this.getReport();
let out = this._standardActions; let out = this._standardActions;
const toFilter = ['cloneYaml', 'goToEditYaml', 'download']; const toFilter = ['cloneYaml', 'goToEditYaml', 'download'];
@ -14,36 +16,67 @@ export default {
} }
}); });
const t = this.$rootGetters['i18n/t'];
const downloadReport = { const downloadReport = {
action: 'downloadReport', action: 'downloadLatestReport',
enabled: this.hasReport, enabled: this.hasReport,
icon: 'icon icon-fw icon-chevron-right', icon: 'icon icon-fw icon-download',
label: 'Download Report', label: t('cis.downloadLatestReport'),
total: 1, total: 1,
}; };
out.unshift({ divider: true }); const downloadAllReports = {
out.unshift(downloadReport); action: 'downloadAllReports',
enabled: this.hasReport,
icon: 'icon icon-fw icon-download',
label: t('cis.downloadAllReports'),
total: 1,
};
if (this.hasReports) {
out.unshift({ divider: true });
if (this.spec?.cronSchedule) {
out.unshift(downloadAllReports);
}
out.unshift(downloadReport);
}
return out; return out;
}, },
hasReport: false, applyDefaults() {
return () => {
const spec = this.spec || {};
getReport() { spec.scanProfileName = null;
return async() => { spec.scanAlertRule = {};
const owned = await this.getOwned(); spec.scoreWarning = 'pass';
const reportCRD = owned.filter(each => each.type === CIS.REPORT)[0]; set(this, 'spec', spec);
this.hasReport = !!reportCRD;
return reportCRD;
}; };
}, },
downloadReport() { hasReports() {
const { relationships = [] } = this.metadata;
const reportRel = findBy(relationships, 'toType', CIS.REPORT);
return !!reportRel;
},
getReports() {
return async() => { return async() => {
const report = await this.getReport(); const owned = await this.getOwned();
const reportCRDs = owned.filter(each => each.type === CIS.REPORT);
return reportCRDs;
};
},
downloadLatestReport() {
return async() => {
const reports = await this.getReports() || [];
const report = sortBy(reports, 'metadata.creationTimestamp', true)[0];
const Papa = await import(/* webpackChunkName: "cis" */'papaparse'); const Papa = await import(/* webpackChunkName: "cis" */'papaparse');
try { try {
@ -56,4 +89,29 @@ export default {
} }
}; };
}, },
downloadAllReports() {
return async() => {
const toZip = {};
const reports = await this.getReports() || [];
const Papa = await import(/* webpackChunkName: "cis" */'papaparse');
reports.forEach((report) => {
try {
const testResults = report.aggregatedTests;
const csv = Papa.unparse(testResults);
toZip[`${ report.id }.csv`] = csv;
} catch (err) {
this.$dispatch('growl/fromError', { title: 'Error downloading file', err }, { root: true });
}
});
if (!isEmpty(toZip)) {
generateZip(toZip).then((zip) => {
downloadFile(`${ this.id }-reports`, zip, 'application/zip');
});
}
};
}
}; };

View File

@ -0,0 +1,14 @@
export default {
warnDeletionMessage() {
return (toRemove = []) => {
return this.$rootGetters['i18n/t']('cis.deleteProfileWarning', { count: toRemove.length });
};
},
numberTestsSkipped() {
const { skipTests = [] } = this.spec;
return skipTests.length;
}
};

View File

@ -803,7 +803,7 @@ export default {
<div class="row mb-20"> <div class="row mb-20">
<div class="col span-6"> <div class="col span-6">
<LabeledSelect <LabeledSelect
label="Chart" :label="t('catalog.install.chart')"
:value="chart" :value="chart"
:options="charts" :options="charts"
:get-option-label="opt => getOptionLabel(opt)" :get-option-label="opt => getOptionLabel(opt)"
@ -813,7 +813,7 @@ export default {
</div> </div>
<div v-if="chart" class="col span-6"> <div v-if="chart" class="col span-6">
<LabeledSelect <LabeledSelect
label="Version" :label="t('catalog.install.version')"
:value="$route.query.version" :value="$route.query.version"
option-label="version" option-label="version"
option-key="version" option-key="version"

View File

@ -779,7 +779,7 @@ export const getters = {
}; };
}, },
headersFor(state) { headersFor(state, getters, rootState, rootGetters) {
return (schema) => { return (schema) => {
const attributes = schema.attributes || {}; const attributes = schema.attributes || {};
const columns = attributes.columns || []; const columns = attributes.columns || [];
@ -790,7 +790,7 @@ export const getters = {
if ( typeof entry === 'string' ) { if ( typeof entry === 'string' ) {
const col = findBy(columns, 'name', entry); const col = findBy(columns, 'name', entry);
if ( col ) { if ( col ) {
return fromSchema(col); return fromSchema(col, rootGetters);
} else { } else {
return null; return null;
} }
@ -812,7 +812,7 @@ export const getters = {
out.push(NAMESPACE); out.push(NAMESPACE);
} }
} else { } else {
out.push(fromSchema(col)); out.push(fromSchema(col, rootGetters));
} }
} }
@ -831,7 +831,7 @@ export const getters = {
return out; return out;
function fromSchema(col) { function fromSchema(col, rootGetters) {
let formatter, width, formatterOpts; let formatter, width, formatterOpts;
if ( (col.format === '' || col.format == 'date') && col.name === 'Age' ) { if ( (col.format === '' || col.format == 'date') && col.name === 'Age' ) {
@ -848,9 +848,13 @@ export const getters = {
formatter = 'Number'; formatter = 'Number';
} }
const exists = rootGetters['i18n/exists']
const t = rootGetters['i18n/t']
const labelKey = `tableHeaders.${col.name}`
return { return {
name: col.name.toLowerCase(), name: col.name.toLowerCase(),
label: col.name, label: exists(labelKey) ? t(labelKey) : col.name,
value: col.field.startsWith('.') ? `$${ col.field }` : col.field, value: col.field.startsWith('.') ? `$${ col.field }` : col.field,
sort: [col.field], sort: [col.field],
formatter, formatter,

View File

@ -7,15 +7,13 @@ export async function downloadFile(fileName, content, contentType = 'text/plain;
return saveAs(blob, fileName); return saveAs(blob, fileName);
} }
// [{name: 'file1', file: 'data'}, {name: 'file2', file: 'data2'}] // {[fileName1]:data1, [fileName2]:data2}
export function generateZip(files) { export function generateZip(files) {
// Moving this to a dynamic const JSZip = import('jszip') didn't work... figure out later // Moving this to a dynamic const JSZip = import('jszip') didn't work... figure out later
const zip = new JSZip(); const zip = new JSZip();
for ( const fileName in files) { for ( const fileName in files) {
const file = files[fileName]; zip.file(fileName, files[fileName]);
zip.file(fileName, file.data);
} }
return zip.generateAsync({ type: 'blob' }).then((contents) => { return zip.generateAsync({ type: 'blob' }).then((contents) => {