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/tooltip";
@import "./global/table";
@import "./global/select";
/* @import 'vue-select/src/scss/vue-select.scss'; */
@import "./vendor/vue-select";
@import "./vendor/vue-js-modal";
@import '@/node_modules/xterm/css/xterm.css';

View File

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

View File

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

View File

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

View File

@ -13,7 +13,6 @@
transition-timing-function: ease-in-out;
color: var(--input-label);
pointer-events: none;
z-index: z-index('overContent');
i {
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;
}
}
// 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-text: #{$lightest};
--scrollbar-thumb: #{$medium};
--scrollbar-thumb-dropdown: #{$darker};
--scrollbar-thumb-dropdown: #{$medium};
--nav-bg: #{$darkest};
--nav-active: #{rgba($primary, 0.3)};

View File

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

View File

@ -109,6 +109,7 @@ assignTo:
=1 { Assign Cluster To… }
other { Assign {count} Clusters To… }
}
workspace: Workspace
asyncButton:
default:
@ -327,21 +328,41 @@ chartHeading:
cis:
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
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.
profile: Profile
retention: Retention Count
testID: Test ID
testsToSkip: Tests to Skip
testsSkipped: Tests Skipped
scan:
description: Description
failed: Failed
fail: Fail
lastScanTime: Last Scan Time
notApplicable: 'N/A'
number: Number
passed: Passed
pass: Pass
scanReport: Scan Report
skipped: Skipped
skip: Skip
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:
nodeDriver:
@ -871,6 +892,24 @@ monitoring:
fields:
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:
name:
label: Name
@ -887,6 +926,16 @@ nameNsDescription:
namespace:
containerResourceLimit: Container Resource Limit
project:
label: Project
namespaceFilter:
selected:
label: "{total} items selected"
namespaceList:
selectLabel: Namespace
addLabel: Add Namespace
node:
detail:
@ -957,6 +1006,39 @@ prefs:
hideDesc:
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:
alertingRules:
addLabel: Add Alert
@ -1321,6 +1403,18 @@ tableHeaders:
version: Version
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:
arrayLength:
between: '"{key}" should contain between {min} and {max} {max, plural, =1 {item} other {items}}'
@ -1489,6 +1583,7 @@ workload:
value:
label: Value
placeholder: e.g. BAR
tty: TTY
workingDir: WorkingDir
stdin: Stdin
healthCheck:
@ -1669,6 +1764,7 @@ workload:
inNamespaces: "Pods in these namespaces:"
key: Key
lessThan: <
namespaces: Namespaces
notIn:
operator: Operator
value: Value
@ -1758,6 +1854,7 @@ workload:
storagePolicyName: Storage Policy Name
volumePath: Volume Path
defaultMode: Default Mode
driver: driver
hostPath:
label: The Path on the Node must be
options:
@ -2046,3 +2143,7 @@ workloadPorts:
podAffinity:
addLabel: Add Pod Selector
keyValue:
keyPlaceholder: e.g. foo
valuePlaceholder: e.g. bar

View File

@ -962,7 +962,7 @@ servicePorts:
label: 协议
target:
label: 目标端口
placeholder: 例如80 or http
placeholder: 例如80 http
serviceTypes:
clusterip: 集群IP地址
@ -1130,6 +1130,23 @@ tableHeaders:
value:
version: 版本号
weight: 权重 ## doublecheck这里的权重有什么特殊意义吗
Capacity: 容量
'Access Modes': 访问模式
'Reclaim Policy': 重声明策略
Status: 状态
Claim: 声明
StorageClass: 存储类
Reason: 原因
VolumeMode: 存储卷模式
Targets: 目标
MinPods: 最小Pod数量
MaxPods: 最大Pod数量
Replicas: 副本数
Provisioner: 提供商
ReclaimPolicy: 重声明策略
Role: 角色
Users: 用户
Groups: 用户组
validation:
arrayLength:
@ -1659,6 +1676,16 @@ typeLabel:
persistentvolume: 持久卷
service: 服务
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:
download: 下载YAML
@ -1684,3 +1711,7 @@ workloadPorts:
podAffinity:
addLabel: 添加 Pod Selector
keyValue:
keyPlaceholder: '例如: foo'
valuePlaceholder: '例如: bar'

View File

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

View File

@ -1,101 +1,235 @@
<script>
import { get } from '@/utils/object';
import isString from 'lodash/isString';
import VueSelectOverrides from '@/mixins/vue-select-overrides';
export default {
props: {
size: {
mixins: [VueSelectOverrides],
props: {
buttonLabel: {
default: '',
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
dualAction: {
closeOnSelect: {
default: true,
type: Boolean
},
disabled: {
default: false,
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,
},
},
computed: {
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;
}
data() {
return { focused: false };
},
methods: {
hasSlot(name = 'default') {
return !!this.$slots[name] || !!this.$scopedSlots[name];
ddButtonAction(option) {
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
togglePopover() {
this.$refs.popoverButton.click();
onFocus() {
return this.onFocusLabeled();
},
}
onFocusLabeled() {
this.focused = true;
},
onBlur() {
return this.onBlurLabeled();
},
onBlurLabeled() {
this.focused = false;
},
focusSearch() {
this.$nextTick(() => {
this.$refs['button-dropdown'].searchEl.focus();
});
},
get,
},
};
</script>
<template>
<div class="dropdown-button-group">
<div
class="dropdown-button bg-primary"
:class="{'one-action':!dualAction, [buttonSize]:true}"
>
<slot v-if="dualAction" name="button-content" :buttonSize="buttonSize">
<button
class="bg-transparent"
:class="buttonSize"
disabled="true"
type="button"
>
Button
</button>
</slot>
<div
v-else
:class="buttonSize"
<v-select
ref="button-dropdown"
class="button-dropdown btn"
:class="{
disabled,
focused,
'btn-sm': size === 'sm',
'btn-lg': size === 'lg',
}"
v-bind="$attrs"
:searchable="false"
:clearable="false"
:close-on-select="closeOnSelect"
:filterable="false"
:value="buttonLabel"
:options="dropdownOptions"
:map-keydown="mappedKeys"
: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"
class="dropdown-button-two btn"
:class="{
'btn-sm': size === 'sm',
'btn-lg': size === 'lg',
}"
@click="ddButtonAction(option)"
@focus="focusSearch"
>
<slot name="button-content" />
</div>
<div v-if="hasSlot('popover-content') && dualAction" class="button-divider"></div>
<v-popover
v-if="hasSlot('popover-content')"
placement="bottom"
:container="false"
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>
{{ option.label }}
</button>
</template>
<!-- Pass down templates provided by the caller -->
<template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope">
<slot v-if="slot !== 'selected-option'" :name="slot" v-bind="scope" />
</template>
</v-select>
</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 { get, isEmpty } from '@/utils/object';
import { NAMESPACE, RIO } from '@/config/types';
import Card from '@/components/Card';
import InfoBox from '@/components/InfoBox';
import { alternateLabel } from '@/utils/platform';
import LinkDetail from '@/components/formatter/LinkDetail';
import { uniq } from '@/utils/array';
export default {
components: { Card, LinkDetail },
components: { InfoBox, LinkDetail },
data() {
return { confirmName: '', error: '' };
return {
confirmName: '', error: '', warning: '', preventDelete: false
};
},
computed: {
names() {
@ -54,20 +56,6 @@ export default {
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() {
const remaining = this.toRemove.length - this.names.length;
@ -126,6 +114,31 @@ export default {
} else {
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"
styles="background-color: var(--nav-bg); border-radius: var(--border-radius); max-height: 100vh;"
>
<Card>
<InfoBox>
<h4 slot="title" class="text-default-text">
Are you sure?
</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>
</template>
</template>
<span class="text-warning">
{{ warning }}
</span>
<span v-if="needsConfirm" :key="resource">Re-enter its name below to confirm:</span>
</div>
<input v-if="needsConfirm" id="confirm" v-model="confirmName" type="text" />
<span class="text-warning">{{ preventDeletionMessage }}</span>
<span class="text-error">{{ error }}</span>
<span v-if="!needsConfirm" class="text-info mt-20">{{ protip }}</span>
</div>
@ -242,11 +258,11 @@ export default {
<button class="btn role-secondary" @click="close">
Cancel
</button>
<button class="btn bg-error" :disabled="isDeleteDisabled" @click="remove">
<button class="btn bg-error" :disabled="preventDelete" @click="remove">
Delete
</button>
</template>
</Card>
</InfoBox>
</modal>
</template>

View File

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

View File

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

View File

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

View File

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

View File

@ -2,13 +2,17 @@
import labeledFormElement from '@/mixins/labeled-form-element';
import LabeledInput from '@/components/form/LabeledInput';
import LabeledSelect from '@/components/form/LabeledSelect';
import Select from '@/components/form/Select';
import UnitInput from '@/components/form/UnitInput';
export default {
components: {
LabeledInput, LabeledSelect, UnitInput
LabeledInput,
LabeledSelect,
UnitInput,
Select,
},
mixins: [labeledFormElement],
props: {
mixins: [labeledFormElement],
props: {
disabled: {
type: Boolean,
default: false,
@ -26,22 +30,22 @@ export default {
selectLabel: {
type: String,
default: ''
default: '',
},
selectValue: {
type: String,
default: null
default: null,
},
optionLabel: {
type: String,
default: 'label'
default: 'label',
},
options: {
type: Array,
required: true
required: true,
},
selectBeforeText: {
@ -51,34 +55,37 @@ export default {
textLabel: {
type: String,
default: ''
default: '',
},
textRequired: {
type: Boolean,
default: false
default: false,
},
textValue: {
type: [String, Number],
default: ''
default: '',
},
placeholder: {
type: String,
default: ''
default: '',
},
},
data() {
return { selected: this.selectValue || this.options[0], string: this.textValue };
return {
selected: this.selectValue || this.options[0],
string: this.textValue,
};
},
methods: {
focus() {
const comp = this.$refs.text;
if ( comp ) {
if (comp) {
comp.focus();
}
},
@ -86,29 +93,57 @@ export default {
change() {
this.$emit('input', { selected: this.selected, text: this.string });
},
}
},
};
</script>
<template>
<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 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
v-if="selectLabel"
v-model="selected"
:label="selectLabel"
:class="{'in-input': !isView}"
:class="{ 'in-input': !isView }"
:options="options"
:searchable="searchable"
:disbaled="isView"
:searchable="false"
:clearable="false"
:disabled="disabled"
:disabled="disabled || isView"
:taggable="taggable"
:create-option="name => ({ label: name, value: name })"
:create-option="(name) => ({ label: name, value: name })"
:multiple="false"
:mode="mode"
: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"
@input="change"
/>
@ -143,112 +178,82 @@ export default {
</div>
</template>
<style lang='scss'>
.input-container {
display: flex;
height: 52px;
<style lang='scss' scoped>
.input-container {
display: flex;
height: 52px;
&.select-after {
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;
}
}
}
}
&.select-after {
flex-direction: row-reverse;
& .input-string {
padding-right: 8px;
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;
border-radius: var(--border-radius) 0 0 var(--border-radius);
border-right: 0;
border-left: 1px solid var(--border);
}
.in-input {
margin-right: 0;
border-radius: var(--border-radius) 0 0 var(--border-radius);
& .in-input {
border-radius: 0 var(--border-radius) var(--border-radius) 0;
border-left: 0;
border-right: 1px solid var(--border);
&.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;
width: 40%;
.vs__dropdown-option {
padding: 3px 5px;
}
}
.vs__dropdown-toggle {
color: var(--primary) !important;
height: 100%;
background-color: var(--accent-btn);
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);
}
border-radius: var(--border-radius) 0 0 var(--border-radius);
}
}
}
}
</style>

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@ export default {
},
extraColumns: {
type: Array,
default: () => []
default: () => [],
},
nameLabel: {
@ -106,31 +106,31 @@ export default {
const metadata = v.metadata;
let namespace, name, description;
if (this.nameKey ) {
if (this.nameKey) {
name = get(v, this.nameKey);
} else {
name = metadata.name;
}
if ( this.namespaced ) {
if ( this.forceNamespace ) {
if (this.namespaced) {
if (this.forceNamespace) {
namespace = this.forceNamespace;
this.updateNamespace(namespace);
} else if ( this.namespaceKey ) {
} else if (this.namespaceKey) {
namespace = get(v, this.namespaceKey);
} else {
namespace = metadata?.namespace;
}
if ( !namespace ) {
if (!namespace) {
namespace = this.$store.getters['defaultNamespace'];
if ( metadata ) {
if (metadata) {
metadata.namespace = namespace;
}
}
}
if ( this.descriptionKey ) {
if (this.descriptionKey) {
description = get(v, this.descriptionKey);
} else {
description = metadata?.annotations?.[DESCRIPTION];
@ -139,34 +139,39 @@ export default {
return {
namespace,
name,
description
description,
};
},
computed: {
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() {
return this.nameDisabled || ( this.mode === _EDIT && !this.nameEditable);
return this.nameDisabled || (this.mode === _EDIT && !this.nameEditable);
},
namespaces() {
const inStore = this.$store.getters['currentProduct'].inStore;
const choices = this.$store.getters[`${ inStore }/all`](this.namespaceType);
const out = sortBy(choices.map((obj) => {
return {
label: obj.nameDisplay,
value: obj.id,
};
}), 'label');
const out = sortBy(
choices.map((obj) => {
return {
label: obj.nameDisplay,
value: obj.id,
};
}),
'label'
);
if ( this.forceNamespace ) {
if (this.forceNamespace) {
out.unshift({
label: this.forceNamespace,
value: this.forceNamespace
value: this.forceNamespace,
});
}
@ -189,7 +194,7 @@ export default {
name(val) {
val = val.toLowerCase();
if ( this.nameKey ) {
if (this.nameKey) {
set(this.value, this.nameKey, val);
} else {
this.$set(this.value.metadata, 'name', val);
@ -203,7 +208,7 @@ export default {
},
description(val) {
if ( this.descriptionKey ) {
if (this.descriptionKey) {
set(this.value, this.descriptionKey, val);
} else {
this.value.setAnnotation(DESCRIPTION, val);
@ -222,11 +227,11 @@ export default {
methods: {
updateNamespace(val) {
if ( this.forceNamespace ) {
if (this.forceNamespace) {
val = this.forceNamespace;
}
if ( this.namespaceKey ) {
if (this.namespaceKey) {
set(this.value, this.namespaceKey, val);
} else {
this.value.metadata.namespace = val;
@ -236,19 +241,20 @@ export default {
changeNameAndNamespace(e) {
this.name = (e.text || '').toLowerCase();
this.namespace = e.selected;
}
}
},
},
};
</script>
<template>
<div>
<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">
<InputWithSelect
v-if="namespaced"
ref="name"
class="namespace-select"
:mode="mode"
:disabled="namespaceReallyDisabled"
:text-label="t(nameLabel)"
@ -277,7 +283,7 @@ export default {
/>
</slot>
</div>
<div :class="{col: true, [colSpan]: true}">
<div :class="{ col: true, [colSpan]: true }">
<LabeledInput
key="description"
v-model="description"
@ -287,10 +293,24 @@ export default {
:min-height="30"
/>
</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>
</div>
</div>
</div>
</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-model="kind"
:mode="mode"
:label="t('generic.type')"
:label="t('probe.type.label')"
:options="kindOptions"
placeholder="Select a check type"
:placeholder="t('probe.type.placeholder')"
/>
<div v-if="kind && kind!=='none'" class="spacer-small" />
@ -170,8 +170,8 @@ export default {
min="1"
max="65535"
:mode="mode"
:label="t('workload.container.healthCheck.httpGet.port')"
placeholder="e.g. 80"
:label="t('probe.httpGet.port.label')"
:placeholder="t('probe.httpGet.port.placeholder')"
/>
<div class="spacer-small" />
@ -179,8 +179,8 @@ export default {
<LabeledInput
v-model="httpGet.path"
:mode="mode"
:label="t('workload.container.healthCheck.httpGet.path')"
placeholder="e.g. /healthz"
:label="t('probe.httpGet.path.label')"
:placeholder="t('probe.httpGet.path.placeholder')"
/>
</div>
@ -191,8 +191,8 @@ export default {
min="1"
max="65535"
:mode="mode"
:label="t('workload.container.healthCheck.httpGet.port')"
placeholder="e.g. 25"
:label="t('probe.httpGet.port.label')"
:placeholder="t('probe.httpGet.port.placeholderDuex')"
/>
<div class="spacer-small" />
</div>
@ -201,8 +201,8 @@ export default {
<div class="col span-12">
<ShellInput
v-model="exec.command"
:label="t('workload.container.healthCheck.command.command')"
placeholder="e.g. cat /tmp/health"
:label="t('probe.httpGet.port.command.label')"
:placeholder="t('probe.httpGet.port.command.placeholder')"
/>
</div>
<div class="spacer-small" />
@ -219,30 +219,30 @@ export default {
<UnitInput
v-model="probe.periodSeconds"
:mode="mode"
:label="t('workload.container.healthCheck.checkInterval')"
:label="t('probe.checkInterval.label')"
min="1"
:suffix="t('suffix.sec')"
placeholder="Default: 10"
:placeholder="t('probe.checkInterval.placeholder')"
/>
</div>
<div class="col span-4">
<UnitInput
v-model="probe.initialDelaySeconds"
:mode="mode"
:label="t('workload.container.healthCheck.initialDelay')"
:suffix="t('suffix.sec')"
:label="t('probe.initialDelay.label')"
min="0"
placeholder="Default: 0"
:placeholder="t('probe.initialDelay.placeholder')"
/>
</div>
<div class="col span-4">
<UnitInput
v-model="probe.timeoutSeconds"
:mode="mode"
:label="t('workload.container.healthCheck.timeout')"
:suffix="t('suffix.sec')"
:label="t('probe.timeout.placeholder')"
min="0"
placeholder="Default: 3"
:placeholder="t('probe.timeout.placeholder')"
/>
</div>
</div>
@ -256,8 +256,8 @@ export default {
type="number"
min="1"
:mode="mode"
:label="t('workload.container.healthCheck.successThreshold')"
placeholder="Default: 1"
:label="t('probe.successThreshold.label')"
:placeholder="t('probe.successThreshold.placeholder')"
/>
</div>
<div class="col span-6">
@ -266,8 +266,8 @@ export default {
type="number"
min="1"
:mode="mode"
:label="t('workload.container.healthCheck.failureThreshold')"
placeholder="Default: 3"
:label="t('probe.failureThreshold.label')"
:placeholder="t('probe.failureThreshold.label')"
/>
</div>
</div>
@ -284,14 +284,14 @@ export default {
:pad-left="false"
:as-map="false"
:read-allowed="false"
:title="t('workload.container.healthCheck.httpGet.headers')"
:title="t('probe.httpGet.headers.label')"
:key-label="t('generic.name')"
:value-label="t('generic.value')"
:add-label="t('generic.add')"
>
<template #title>
<h4>
{{ t('workload.container.healthCheck.httpGet.headers') }}
{{ t('probe.httpGet.headers.label') }}
</h4>
</template>
</KeyValue>
@ -308,5 +308,8 @@ export default {
.title {
margin-bottom: 10px;
}
::v-deep .labeled-select {
height: auto;
}
</style>

View File

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

View File

@ -1,16 +1,88 @@
<script>
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 {
props: {
placement: {
mixins: [LabeledFormElement, VueSelectOverrides],
props: {
disabled: {
default: false,
type: Boolean,
},
mode: {
default: 'edit',
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: {
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 }) {
if (this.popperOverride) {
return this.popperOverride(dropdownList, component, { width });
}
/**
* We need to explicitly define the dropdown width since
* it is usually inherited from the parent with CSS.
@ -28,12 +100,11 @@ export default {
* above.
*/
const popper = createPopper(component.$refs.toggle, dropdownList, {
placement: this.placement,
modifiers: [
{
name: 'offset',
options: { offset: [0, -1] }
options: { offset: [0, 2] },
},
{
name: 'toggleClass',
@ -43,7 +114,7 @@ export default {
component.$el.setAttribute('x-placement', state.placement);
},
},
]
],
});
/**
@ -52,17 +123,56 @@ export default {
*/
return () => popper.destroy();
},
focusSearch() {
this.$nextTick(() => {
this.$refs['select-input'].searchEl.focus();
});
},
get,
},
};
</script>
<template>
<v-select
v-bind="$attrs"
append-to-body
:calculate-position="placement ? withPopper : undefined"
v-on="$listeners"
<div
class="unlabeled-select"
:class="{
disabled: disabled && !isView,
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,60 +3,35 @@ import { NAMESPACE_FILTERS } from '@/store/prefs';
import { NAMESPACE, MANAGEMENT } from '@/config/types';
import { sortBy } from '@/utils/sort';
import { isArray, addObjects, findBy, filterBy } from '@/utils/array';
import Select from '@/components/form/Select';
export default {
components: { Select },
data() {
return {
isHovered: false,
hoveredTimeout: null,
maskedWidth: null,
};
},
computed: {
value: {
get() {
const values = this.$store.getters['prefs/get'](NAMESPACE_FILTERS);
const options = this.options;
filterIsHovered() {
return this.isHovered;
},
if ( !values ) {
return [];
}
maskedDropdownWidth() {
const refs = this.$refs;
const select = refs.select;
// Remove values that are not valid options
const out = values.map((value) => {
return findBy(options, 'id', value);
}).filter(x => !!x);
if (select) {
const selectWidth = select.$el.offsetWidth;
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);
return selectWidth;
}
return null;
},
options() {
@ -64,63 +39,70 @@ export default {
const out = [
{
id: 'all',
kind: 'special',
label: t('nav.ns.all'),
id: 'all',
kind: 'special',
label: t('nav.ns.all'),
},
{
id: 'all://user',
kind: 'special',
label: t('nav.ns.user'),
id: 'all://user',
kind: 'special',
label: t('nav.ns.user'),
},
{
id: 'all://system',
kind: 'special',
label: t('nav.ns.system'),
id: 'all://system',
kind: 'special',
label: t('nav.ns.system'),
},
{
id: 'namespaced://true',
kind: 'special',
label: t('nav.ns.namespaced'),
id: 'namespaced://true',
kind: 'special',
label: t('nav.ns.namespaced'),
},
{
id: 'namespaced://false',
kind: 'special',
label: t('nav.ns.clusterLevel'),
}
id: 'namespaced://false',
kind: 'special',
label: t('nav.ns.clusterLevel'),
},
];
divider();
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'];
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 namespacesByProject = {};
let firstProject = true;
namespacesByProject[null] = []; // For namespaces not in a project
for ( const project of projects ) {
for (const project of projects) {
projectsById[project.metadata.name] = project;
}
for (const namespace of namespaces ) {
for (const namespace of namespaces) {
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
projectId = null;
}
let entry = namespacesByProject[namespace.projectId];
if ( !entry ) {
if (!entry) {
entry = [];
namespacesByProject[namespace.projectId] = entry;
}
@ -128,10 +110,10 @@ export default {
entry.push(namespace);
}
for ( const project of projects ) {
for (const project of projects) {
const id = project.metadata.name;
if ( firstProject ) {
if (firstProject) {
firstProject = false;
} else {
divider();
@ -140,7 +122,7 @@ export default {
out.push({
id: `project://${ id }`,
kind: 'project',
label: t('nav.ns.project', { name: project.nameDisplay }),
label: t('nav.ns.project', { name: project.nameDisplay }),
});
const forThisProject = namespacesByProject[id] || [];
@ -150,8 +132,8 @@ export default {
const orphans = namespacesByProject[null];
if ( orphans.length ) {
if ( !firstProject ) {
if (orphans.length) {
if (!firstProject) {
divider();
}
@ -171,17 +153,20 @@ export default {
return out;
function addNamespace(namespaces) {
if ( !isArray(namespaces) ) {
if (!isArray(namespaces)) {
namespaces = [namespaces];
}
addObjects(out, namespaces.map((namespace) => {
return {
id: `ns://${ namespace.id }`,
kind: 'namespace',
label: t('nav.ns.namespace', { name: namespace.nameDisplay }),
};
}));
addObjects(
out,
namespaces.map((namespace) => {
return {
id: `ns://${ namespace.id }`,
kind: 'namespace',
label: t('nav.ns.namespace', { name: namespace.nameDisplay }),
};
})
);
}
function divider() {
@ -191,81 +176,147 @@ export default {
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: {
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>
<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>
<div class="filter">
<v-select
<div
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"
v-model="value"
:class="{
'has-more': showGroupedOptions,
}"
multiple
:placeholder="t('nav.ns.all')"
:selectable="option => !option.disabled && option.id"
:selectable="(option) => !option.disabled && option.id"
:options="options"
label="label"
:close-on-select="false"
@on-blur="focusHandler('selectBlurred')"
>
<template v-slot:option="opt">
<template v-if="opt.kind === 'namespace'">
@ -281,7 +332,99 @@ export default {
{{ opt.label }}
</template>
</template>
</v-select>
</Select>
<button v-shortkey.once="['n']" class="hide" @shortkey="focus()" />
</div>
</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 $ from 'jquery';
import { CATALOG } from '@/config/types';
import Select from '@/components/form/Select';
export default {
components: { Select },
data() {
return { previous: null };
},
@ -162,6 +165,10 @@ export default {
const popper = createPopper(component.$refs.toggle, dropdownList, {
strategy: 'fixed',
modifiers: [
{
name: 'offset',
options: { offset: [0, -2] },
},
{
name: 'toggleClass',
enabled: true,
@ -181,18 +188,17 @@ export default {
<template>
<div class="filter">
<v-select
<Select
ref="select"
key="product"
:value="value"
:clearable="false"
:searchable="false"
:selectable="option => !option.disabled"
:options="options"
:reduce="opt=>opt.value"
:calculate-position="withPopper"
:popper-override="withPopper"
:append-to-body="true"
label="label"
placement="bottom"
@input="change"
>
<template v-slot:option="opt">
@ -205,7 +211,7 @@ export default {
<i v-if="opt.kind === 'external'" class="icon icon-external-link ml-10" />
</template>
</template>
</v-select>
</Select>
<button v-shortkey.once="['p']" class="hide" @shortkey="focus()" />
<button v-shortkey.once="['f']" class="hide" @shortkey="switchToFleet()" />
<button v-shortkey.once="['e']" class="hide" @shortkey="switchToExplorer()" />
@ -214,83 +220,87 @@ export default {
</template>
<style lang="scss" scoped>
.filter ::v-deep .v-select {
max-width: 100%;
display: inline-block;
.filter ::v-deep .unlabeled-select {
background-color: transparent;
&:not(.focused) {
border: var(--outline-width) solid transparent;
}
.v-select {
&.vs--disabled .vs__actions {
display: none;
}
.vs__actions:after {
fill: white !important;
color: white !important;
.vs__actions {
&:after {
fill: white !important;
color: white !important;
}
}
.vs__dropdown-toggle {
margin-bottom: -4px;
height: var(--header-height);
// margin-left: 35px;
background-color: transparent;
border: 0;
position: relative;
// left: 35px;
padding-top: 0;
}
.vs__selected {
user-select: none;
cursor: default;
color: white;
line-height: calc(var(--header-height) - 14px);
line-height: calc(var(--header-height) - 7px);
position: relative;
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 lang="scss">
.product-menu {
width: 300px;
max-height: 90vh;
// 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
.product-menu {
width: 300px;
max-height: 90vh;
&.vs__dropdown-menu {
outline: none;
}
.vs__dropdown-option {
padding: 10px;
text-decoration: none;
border-left: 5px solid transparent;
.vs__dropdown-option {
padding: 10px;
text-decoration: none;
border-left: 5px solid transparent;
&.vs__dropdown-option--disabled {
// The dividers
padding: 0;
}
.product-icon {
color: var(--product-icon);
margin-right: 5px;
}
UL {
margin: 0;
}
&.vs__dropdown-option--disabled {
// The dividers
padding: 0;
}
.vs__dropdown-option--selected {
color: var(--body-text);
// font-weight: bold;
background: var(--nav-active);
border-left: 5px solid var(--primary);
.product-icon {
color: var(--product-icon);
margin-right: 5px;
}
.product-icon {
color: var(--product-icon-active);
}
UL {
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>

View File

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

View File

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

View File

@ -17,7 +17,7 @@ export default {
set(value) {
this.$store.commit('updateWorkspace', { value });
this.$store.dispatch('prefs/set', { key: WORKSPACE, value });
}
},
},
options() {
@ -29,7 +29,7 @@ export default {
});
return out;
}
},
},
methods: {
@ -38,48 +38,8 @@ export default {
},
},
};
</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>
<div class="filter">
<Select
@ -88,8 +48,69 @@ export default {
label="label"
:options="options"
:clearable="false"
:reduce="opt=>opt.value"
:reduce="(opt) => opt.value"
/>
<button v-shortkey.once="['w']" class="hide" @shortkey="focus()" />
</div>
</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'],
},
{
name: 'total',
label: 'Total',
value: 'status.summary.total',
name: 'total',
labelKey: 'cis.scan.total',
value: 'status.summary.total',
},
{
name: 'pass',
label: 'Pass',
value: 'status.summary.pass',
name: 'pass',
value: 'status.summary.pass',
labelKey: 'cis.scan.pass',
},
{
name: 'fail',
label: 'Fail',
value: 'status.summary.fail',
name: 'fail',
labelKey: 'cis.scan.fail',
value: 'status.summary.fail',
},
{
name: 'skip',
label: 'Skip',
value: 'status.summary.skip',
name: 'warn',
labelKey: 'cis.scan.warn',
value: 'status.summary.warn',
},
{
name: 'notApplicable',
label: 'N/A',
value: 'status.summary.notApplicable',
name: 'skip',
labelKey: 'cis.scan.skip',
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',
@ -79,4 +94,22 @@ export function init(store) {
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 SortableTable from '@/components/SortableTable';
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 { CIS } from '@/config/types';
import { STATE } from '@/config/table-headers';
import { randomStr } from '@/utils/string';
export default {
components: {
Date,
SortableTable,
Banner
Banner,
LabeledSelect
},
props: {
@ -24,7 +28,7 @@ export default {
},
async fetch() {
this.clusterReport = await this.value.getReport();
this.clusterReports = await this.value.getReports();
},
asyncData(ctx) {
@ -32,14 +36,12 @@ export default {
},
data() {
return { clusterReport: null };
return { clusterReports: [], clusterReport: null };
},
computed: {
parsedReport() {
const report = this.clusterReport?.parsedReport;
return report;
return this.clusterReport?.parsedReport || null;
},
reportNodes() {
@ -96,15 +98,19 @@ export default {
value: this.parsedReport.total
},
{
label: this.t('cis.scan.passed'),
label: this.t('cis.scan.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
},
{
label: this.t('cis.scan.failed'),
label: this.t('cis.scan.fail'),
value: this.parsedReport.fail
},
{
@ -168,14 +174,25 @@ export default {
watch: {
value(neu) {
try {
neu.getReport().then((report) => {
this.clusterReport = report;
neu.getReports().then((reports) => {
this.clusterReports = reports;
});
} catch {}
},
clusterReports(neu) {
this.clusterReport = neu[0];
}
},
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 = []) {
if (check.state === 'mixed') {
@ -187,11 +204,13 @@ export default {
testStateSort(state) {
const SORT_ORDER = {
fail: 1,
skip: 2,
notApplicable: 3,
other: 7,
notApplicable: 6,
skip: 5,
pass: 4,
other: 5,
warn: 3,
mixed: 2,
fail: 1,
};
return `${ SORT_ORDER[state] || SORT_ORDER['other'] } ${ state }`;
@ -218,8 +237,17 @@ export default {
<span v-else>{{ item.value }}</span>
</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">
<h3>{{ t('cis.scan.scanReport') }}</h3>
<SortableTable
default-sort-by="state"
:search="false"
@ -269,4 +297,8 @@ export default {
.sub-table {
padding: 0px 40px 0px 40px;
}
.table-header {
align-items: flex-end;
}
</style>

View File

@ -1,17 +1,22 @@
<script>
import CruResource from '@/components/CruResource';
import LabeledSelect from '@/components/form/LabeledSelect';
import LabeledInput from '@/components/form/LabeledInput';
import Banner from '@/components/Banner';
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 createEditView from '@/mixins/create-edit-view';
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');
export default {
components: {
CruResource, LabeledSelect, Banner, Loading
CruResource, LabeledSelect, Banner, Loading, Checkbox, LabeledInput, RadioGroup
},
mixins: [createEditView],
@ -33,9 +38,17 @@ export default {
const hash = await allHash({
profiles: this.$store.dispatch('cluster/findAll', { type: CIS.CLUSTER_SCAN_PROFILE }),
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.defaultConfigMap = hash.defaultConfigMap;
},
@ -44,18 +57,31 @@ export default {
if (!this.value.metadata.name) {
this.value.metadata.generateName = 'scan-';
}
if (!this.value.spec) {
this.value.spec = { scanProfileName: null };
}
return {
allProfiles: [], defaultConfigMap: null, scanProfileName: this.value.spec.scanProfileName
allProfiles: [], defaultConfigMap: null, scanAlertRule: this.value.spec.scanAlertRule, hasAlertManager: false
};
},
computed: {
...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() {
const profileNames = this.allProfiles.filter((profile) => {
const benchmarkVersion = profile?.spec?.benchmarkVersion;
@ -82,13 +108,20 @@ export default {
}
return null;
}
},
monitoringUrl() {
return this.$router.resolve({
name: 'c-cluster-monitoring',
params: { cluster: this.$route.params.cluster }
}).href;
},
},
watch: {
defaultProfile(neu) {
if (neu && !this.scanProfileName) {
this.scanProfileName = neu?.id;
if (neu && !this.value.spec.scanProfileName) {
this.value.spec.scanProfileName = neu?.id;
}
},
},
@ -122,26 +155,56 @@ export default {
<CruResource
v-else
:validation-passed="!!scanProfileName"
:validation-passed="!!value.spec.scanProfileName"
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
@finish="save"
@error="e=>errors = e"
>
<template>
<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">
<LabeledSelect
v-model="scanProfileName"
v-model="value.spec.scanProfileName"
:mode="mode"
:label="t('cis.profile')"
:options="validProfiles"
@input="value.spec.scanProfileName = $event"
/>
</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>
</CruResource>
</template>

View File

@ -98,7 +98,15 @@ export default {
<template>
<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>
<div class="spacer"></div>
<div class="row">

View File

@ -50,6 +50,11 @@ export default {
return {
selectedSeverityLabel: null,
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"
:label="t('prometheusRule.alertingRules.labels.severity.choices.label')"
:localized-label="false"
:options="[
t('prometheusRule.alertingRules.labels.severity.choices.critical'),
t('prometheusRule.alertingRules.labels.severity.choices.warning'),
t('prometheusRule.alertingRules.labels.severity.choices.none'),
]"
:options="severityOptions"
/>
</div>
</div>

View File

@ -67,13 +67,13 @@ export default {
</div>
</div>
<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>
<Tabbed ref="tabbed" :side-tabs="true" default-tab="overview">
<Tab label="Receiver" :weight="2" name="receiver">
<div class="row">
<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>
</Tab>
@ -81,9 +81,9 @@ export default {
<div class="row mb-20">
<div class="col span-6">
<span class="label">
Group By:
{{ t("monitoringRoute.groups.label") }}:
</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>
{{ t('generic.none') }}
</div>
@ -92,32 +92,33 @@ export default {
<hr class="divider" />
<div class="row mb-10">
<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 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 class="row mb-10">
<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>
</Tab>
<Tab label="Matching" :weight="1" name="matching">
<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>
<div v-else class="row">
<div class="col span-12">
<span class="label">
Match:
{{ t('monitoringRoute.matching.label') }}
</span>
<KeyValue
v-if="!isView || Object.keys(value.spec.match || {}).length > 0"
v-model="value.spec.match"
:disabled="value.isRoot"
:options="receiverOptions"
:label="t('monitoringRoute.receiver.label')"
:mode="mode"
:read-allowed="false"
add-label="Add match"
@ -130,13 +131,14 @@ export default {
<div class="row mt-40">
<div class="col span-12">
<span class="label">
Match Regex:
{{ t('monitoringRoute.regex.label') }}:
</span>
<KeyValue
v-if="!isView || Object.keys(value.spec.match_re || {}).length > 0"
v-model="value.spec.match_re"
:disabled="value.isRoot"
:options="receiverOptions"
:label="t('monitoringRoute.receiver.label')"
:mode="mode"
:read-allowed="false"
add-label="Add match regex"

View File

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

View File

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

View File

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

View File

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

View File

@ -11,8 +11,9 @@
*/
import LabeledInput from '@/components/form/LabeledInput';
import Select from '@/components/form/Select';
export default {
components: { LabeledInput },
components: { LabeledInput, Select },
props: {
spec: {
type: Object,
@ -38,7 +39,10 @@ export default {
});
}
return { all };
return {
all,
ruleOperatorOptions: ['add', 'set', 'remove']
};
},
methods: {
format() {
@ -84,7 +88,7 @@ export default {
</tr>
<tr v-for="(rule, i) in all" :key="i">
<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>
<LabeledInput v-model="rule.name" />

View File

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

View File

@ -262,7 +262,7 @@ export default {
<div class="spacer"></div>
<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">
<div id="registry-type" class="row mb-10">
<div class="col span-12">
@ -348,7 +348,7 @@ export default {
key="data"
v-model="value.data"
:mode="mode"
title="Data"
:title="t('secret.data')"
:initial-empty-row="true"
:value-base64="true"
:file-modifier="fileModifier"

View File

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

View File

@ -47,7 +47,7 @@ export default {
<div>
<div class="row mb-10">
<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 v-if="driverComponent" class="mb-10">

View File

@ -55,7 +55,7 @@ export default {
<h3>{{ t('workload.storage.subtypes.csi') }}</h3>
<div class="row mb-10">
<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 v-if="value.csi.driver && driverComponent" class="mb-10">

View File

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

View File

@ -54,10 +54,23 @@ export default {
notView() {
return (this.mode !== _VIEW);
},
isSearchable() {
const { searchable } = this;
const options = ( this.options || [] );
if (searchable || options.length >= 10) {
return true;
}
return false;
},
},
methods: {
onFocus() {
this.$emit('on-focus');
return this.onFocusLabeled();
},
@ -67,6 +80,8 @@ export default {
},
onBlur() {
this.$emit('on-blur');
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 { 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 {
_availableActions() {
this.getReport();
let out = this._standardActions;
const toFilter = ['cloneYaml', 'goToEditYaml', 'download'];
@ -14,36 +16,67 @@ export default {
}
});
const t = this.$rootGetters['i18n/t'];
const downloadReport = {
action: 'downloadReport',
action: 'downloadLatestReport',
enabled: this.hasReport,
icon: 'icon icon-fw icon-chevron-right',
label: 'Download Report',
icon: 'icon icon-fw icon-download',
label: t('cis.downloadLatestReport'),
total: 1,
};
out.unshift({ divider: true });
out.unshift(downloadReport);
const downloadAllReports = {
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;
},
hasReport: false,
applyDefaults() {
return () => {
const spec = this.spec || {};
getReport() {
return async() => {
const owned = await this.getOwned();
const reportCRD = owned.filter(each => each.type === CIS.REPORT)[0];
this.hasReport = !!reportCRD;
return reportCRD;
spec.scanProfileName = null;
spec.scanAlertRule = {};
spec.scoreWarning = 'pass';
set(this, 'spec', spec);
};
},
downloadReport() {
hasReports() {
const { relationships = [] } = this.metadata;
const reportRel = findBy(relationships, 'toType', CIS.REPORT);
return !!reportRel;
},
getReports() {
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');
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="col span-6">
<LabeledSelect
label="Chart"
:label="t('catalog.install.chart')"
:value="chart"
:options="charts"
:get-option-label="opt => getOptionLabel(opt)"
@ -813,7 +813,7 @@ export default {
</div>
<div v-if="chart" class="col span-6">
<LabeledSelect
label="Version"
:label="t('catalog.install.version')"
:value="$route.query.version"
option-label="version"
option-key="version"

View File

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

View File

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