mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Migrating Vue components to React and stores refactoring (#585)
Signed-off-by: Roman <ixrock@gmail.com> Co-authored-by: Sebastian Malton <sebastian@malton.name> Co-authored-by: Sebastian Malton <smalton@mirantis.com> Co-authored-by: Lauri Nevala <lauri.nevala@gmail.com> Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
297 files changed
+8186
-9103
No files matched your search
@@ -1,38 +0,0 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<div id="lens-container" />
|
||||
<div class="draggable-top" />
|
||||
<div class="main-view" :class="{ 'menu-visible': isMenuVisible }">
|
||||
<main-menu v-if="isMenuVisible" />
|
||||
<router-view />
|
||||
</div>
|
||||
<bottom-bar v-if="isMenuVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MainMenu from "@/_vue/components/MainMenu/MainMenu";
|
||||
import BottomBar from "@/_vue/components/BottomBar/BottomBar";
|
||||
|
||||
export default {
|
||||
name: 'Lens',
|
||||
components: {
|
||||
BottomBar,
|
||||
MainMenu
|
||||
},
|
||||
computed: {
|
||||
isMenuVisible: function () { return this.$store.getters.isMenuVisible }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.draggable-top {
|
||||
-webkit-app-region: drag;
|
||||
left: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
height: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,211 +0,0 @@
|
||||
@import "custom";
|
||||
@import "~typeface-roboto/index.css";
|
||||
@import "~material-design-icons/iconfont/material-icons.css";
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
@import "~bootstrap-vue/src/index";
|
||||
@import "~prismjs/themes/prism-tomorrow.css";
|
||||
@import "~vue-prism-editor/dist/VuePrismEditor.css";
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: $lens-main-bg;
|
||||
color: $lens-text-color;
|
||||
font-size: 14px; // font size that is used also on lens UI
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body{
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
pre {
|
||||
color: $lens-text-color-light;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
& > .main-view{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
& > .content{
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
&.menu-visible > .content{
|
||||
left: 70px;
|
||||
bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
appearance: none;
|
||||
border: 1px solid $lens-pane-bg;
|
||||
background-color: $lens-menu-bg;
|
||||
color: $lens-text-color-light;
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAKxWlDQ1BEaXNwbGF5AABIiZWXd1BT+RbHv/emEhJaAgJSQm+CFOkivQSQ3m2EhBJKjCFg7yyuYEVFBCu4KDZcCyBrQSzYFnsvi7LY10VdbKi8P3iEfW/ezJs9M3fuZ75zft9zzm/uH+cCHKZQJssnNYACqUIeFxrAT0lN4zO6QQUTbLhCRSgqlPnHxEQCwND7b0EAH26BAIDr9kKZLB//LDTFmYUigIgBkCEuFBUAxCGAeCOSyRUAZTsAs+kKmQKgnAPAk6ekpgGUBwB42YPcC4CXkZKaBlCpAHjyhLhAgKoLMNlCoTwbYJsD4BeLshUAOwiAo1QskQJsBYBxohyhGGAfAjCqoGCqGGB3AbDO+JtP9n94Zig9hcJsJQ/OAgBgBkkKZfnCmf/wOv5/FOQXDdWwBMDOkYfFAdABiDt5UyOULM2Iih5iiRgY4pyisMQhFhUGpg2xWBgUoTybHxU5xFmSEIHSRyFIGOLMwuD4IZZPjVPWypIH+g+xUD5ctygvUannZAqU/rNyEpKHuFiSFDXEhXnxEcM5gUpdXhSn7D9TGhowXDdEOXtB4d/mlQiUZxU5CWHK2YXD/WdK/Yc9C1OUvYkzg4KHcxKV+TJFgLKWLD9GmZ+ZH6rUC4vjlWcV8oThfEWM8g5zheExQwx/xCMR8eAjBgFwhgfcEQsoMmcoACBwqmymXJKdo+D7y2T5mXyBVOQwiu/s6OQJpKSm8Qc/ifd3QAAgdJjDWr424GkBkDXDWsYroGU5oD4wrFndBNSigLYKUZG8eFCjAgANLKiDBz0YwQzWsIcz3OANPwQjHNFIQComQ4QcFECO6ZiDhShFOVZhHaqxBXXYib04gGYcxUmcxUVcwU3cRxd68Aq9+IB+giAYBIfgEnqEMWFB2BHOhAcxjggmIok4IpVIJ7IJKVFEzCEWE+VEBVFNbCMaiJ+JI8RJ4jxxlbhLPCZeEO+ILySFZJM80pC0JEeTHqQ/GUEmkJPIbHIaOYssIVeQVWQtuYdsIk+SF8mbZBf5iuyjgKJK0aGYUOwpHpRASjQljZJFkVPmUcoolZRayj5KK6WDcp3SRXlN+UylU7lUPtWe6k0NoyZSRdRp1HnUZdRq6k5qE/U09Tr1MbWX+p3GoRnQ7GheNAEthZZNm04rpVXS6mmHaWdoN2k9tA90Ol2HbkV3p4fRU+m59Nn0ZfRN9EZ6G/0qvZvex2Aw9Bh2DB9GNEPIUDBKGRsYexgnGNcYPYxPTFWmMdOZGcJMY0qZi5iVzF3M48xrzGfMfhUNFQsVL5VoFbHKTJWVKttVWlUuq/So9LM0WVYsH1YCK5e1kFXF2sc6w3rAeq+qqmqq6qkaqypRXaBapbpf9ZzqY9XPbC22LTuQPZFdxF7B3sFuY99lv+dwOJYcP04aR8FZwWngnOI84nxS46o5qAnUxGrz1WrUmtSuqb1RV1G3UPdXn6w+S71S/aD6ZfXXGioalhqBGkKNeRo1Gkc0bmv0aXI1nTSjNQs0l2nu0jyv+VyLoWWpFawl1irRqtM6pdXNpXDNuIFcEXcxdzv3DLeHR+dZ8QS8XF45by+vk9erraU9RjtJe4Z2jfYx7S4dio6ljkAnX2elzgGdWzpfRhiO8B+ROWLpiH0jro34qDtS1083U7dMt1H3pu4XPb5esF6e3mq9Zr2H+lR9W/1Y/en6m/XP6L8eyRvpPVI0smzkgZH3DEgDW4M4g9kGdQaXDPoMjQxDDWWGGwxPGb420jHyM8o1Wmt03OiFMdd4nLHEeK3xCeOXfG2+Pz+fX8U/ze81MTAJMyky2WbSadJvamWaaLrItNH0oRnLzMMsy2ytWbtZr7mx+XjzOea7ze9ZqFh4WORYrLfosPhoaWWZbLnEstnyuZWulcBqltVuqwfWHGtf62nWtdY3bOg2HjZ5NptsrtiStq62ObY1tpftSDs3O4ndJruro2ijPEdJR9WOum3Ptve3L7bfbf/YQcch0mGRQ7PDm9Hmo9NGrx7dMfq7o6tjvuN2x/tOWk7hToucWp3eOds6i5xrnG+4cFxCXOa7tLi8HWM3JnPM5jF3XLmu412XuLa7fnNzd5O77XN74W7unu6+0f22B88jxmOZxzlPmmeA53zPo56fvdy8FF4HvP70tvfO897l/Xys1djMsdvHdvuY+gh9tvl0jeOPSx+3dVyXr4mv0LfW94mfmZ/Yr97vmb+Nf67/Hv83AY4B8oDDAR8DvQLnBrYFUYJCg8qCOoO1ghODq4MfhZiGZIfsDukNdQ2dHdoWRguLCFsddltgKBAJGgS94e7hc8NPR7Aj4iOqI55E2kbKI1vHk+PDx68Z/yDKIkoa1RyNaEH0muiHMVYx02J+iaXHxsTWxD6Nc4qbE9cRz42fEr8r/kNCQMLKhPuJ1olFie1J6kkTkxqSPiYHJVckd6WMTpmbcjFVP1WS2pLGSEtKq0/rmxA8Yd2EnomuE0sn3ppkNWnGpPOT9SfnTz42RX2KcMrBdFp6cvqu9K/CaGGtsC9DkLExo1cUKFoveiX2E68Vv8j0yazIfJblk1WR9TzbJ3tN9osc35zKnNeSQEm15G1uWO6W3I950Xk78gbyk/MbC5gF6QVHpFrSPOnpqUZTZ0y9KrOTlcq6pnlNWzetVx4hry8kCicVtih4CpniUpF10Q9Fj4vHFdcUf5qeNP3gDM0Z0hmXZtrOXDrz2ayQWT/Nps4WzW6fYzJn4ZzHc/3nbptHzMuY1z7fbH7J/J4FoQt2LmQtzFv46yLHRRWL/lqcvLi1xLBkQUn3D6E/7C5VK5WX3l7ivWTLj9QfJT92LnVZumHp9zJx2YVyx/LK8q/LRMsuLHdaXrV8YEXWis6Vbis3r6Kvkq66tdp39c4KzYpZFd1rxq9pWstfW7b2r3VT1p2vHFO5ZT1rfdH6rqrIqpYN5htWbfhanVN9syagpnGjwcalGz9uEm+6ttlv874thlvKt3zZKtl6Z1votqZay9rKOnpdcd3T7UnbO37y+KmhXr++vP7bDumOrp1xO083uDc07DLYtXI3ubto94s9E/dc2Ru0t2Wf/b5tjTqN5fuxv2j/y5/Tf751IOJA+0GPg/sOWRzaeJh7uKyJaJrZ1Nuc09zVktpy9Uj4kfZW79bDvzj8suOoydGaY9rHVh5nHS85PnBi1om+Nlnb65PZJ7vbp7TfP5Vy6sbp2NOdZyLOnDsbcvZUh3/HiXM+546e9zp/5ILHheaLbhebLrleOvyr66+HO906my67X2654nml9erYq8ev+V47eT3o+tkbghsXb0bdvHor8dad2xNvd90R33l+N//u23vF9/rvL3hAe1D2UONh5SODR7W/2fzW2OXWdexx0ONLT+Kf3O8Wdb/6vfD3rz0lTzlPK58ZP2t47vz86IuQF1deTnjZ80r2qv916R+af2x8Y/3m0J9+f17qTenteSt/O/Bu2Xu99zv+GvNXe19M36MPBR/6P5Z90vu087PH544vyV+e9U//yvha9c3mW+v3iO8PBgoGBmRCuRAAQAFAZmUB73YAnFSAewVgTRjcqQEAxOB/ADC4g/xvHty7AQBuQB2AZD8gug3YCsBqAcBpA2L8gAQ/kC4uyuffUZjl4jzopdoM0CoHBt4nAwwb4NvtgYH+5oGBb/UA5R7Q9mFwlwcAqgSwrgOxcPN113tn8F/xL2AUEjlEqYDhAAAACXBIWXMAAAsTAAALEwEAmpwYAAAF9mlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDAgNzkuMTYwNDUxLCAyMDE3LzA1LzA2LTAxOjA4OjIxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMTktMTEtMDVUMTU6NTI6MzMrMDI6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMTEtMDVUMTU6NTI6MzMrMDI6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDE5LTExLTA1VDE1OjUyOjMzKzAyOjAwIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjUxMTI1YTgyLWY2YmUtNDY1ZC05NDAyLTk0NWNhZGFjYTkzZSIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjJiNjlhZDExLTI0M2MtMzk0Ny05NDM3LWNiZjkzNTM3ODFiYyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSLine truncated
|
||||
background-repeat: no-repeat, repeat;
|
||||
background-position: right .7em top 50%, 0 0;
|
||||
background-size: .65em auto, 100%;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
box-shadow: inset 0 0 12px $lens-pane-bg;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
box-shadow: inset 0 0 12px $lens-pane-bg;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6{
|
||||
color: #fff;
|
||||
font-size: 20.8px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.table{
|
||||
color: $lens-text-color-light;
|
||||
th{
|
||||
border-top: 1px solid #353a3e;
|
||||
font-weight: 300;
|
||||
color: $lens-text-color;
|
||||
}
|
||||
td{
|
||||
border-top: 1px solid #353a3e;
|
||||
}
|
||||
tr:first-child{
|
||||
th, td{
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card{
|
||||
background: $lens-pane-bg;
|
||||
}
|
||||
|
||||
.help{
|
||||
border-left: 1px solid #353a3e;
|
||||
padding-top: 20px;
|
||||
&:first-child{
|
||||
padding-top: 0;
|
||||
}
|
||||
h3{
|
||||
padding: 0.75rem 0 0.75rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-inner {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.editor {
|
||||
height: 400px;
|
||||
font-size: 12px;
|
||||
pre {
|
||||
background-color: #1E1E1E;
|
||||
}
|
||||
}
|
||||
|
||||
.aligner-center-center{
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wrapper{
|
||||
width:100%;
|
||||
height:100%;
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
.error{
|
||||
|
||||
text-align: center;
|
||||
i {
|
||||
color: #dc3545;
|
||||
font-size: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
// Force BS modals to use main app font etc.
|
||||
.modal-open{
|
||||
@extend #app
|
||||
}
|
||||
|
||||
.popover {
|
||||
background-color: $lens-menu-hl;
|
||||
|
||||
select {
|
||||
appearance: none;
|
||||
border: 1px solid $lens-pane-bg;
|
||||
background-color: $lens-menu-bg;
|
||||
color: $lens-text-color-light;
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAKxWlDQ1BEaXNwbGF5AABIiZWXd1BT+RbHv/emEhJaAgJSQm+CFOkivQSQ3m2EhBJKjCFg7yyuYEVFBCu4KDZcCyBrQSzYFnsvi7LY10VdbKi8P3iEfW/ezJs9M3fuZ75zft9zzm/uH+cCHKZQJssnNYACqUIeFxrAT0lN4zO6QQUTbLhCRSgqlPnHxEQCwND7b0EAH26BAIDr9kKZLB//LDTFmYUigIgBkCEuFBUAxCGAeCOSyRUAZTsAs+kKmQKgnAPAk6ekpgGUBwB42YPcC4CXkZKaBlCpAHjyhLhAgKoLMNlCoTwbYJsD4BeLshUAOwiAo1QskQJsBYBxohyhGGAfAjCqoGCqGGB3AbDO+JtP9n94Zig9hcJsJQ/OAgBgBkkKZfnCmf/wOv5/FOQXDdWwBMDOkYfFAdABiDt5UyOULM2Iih5iiRgY4pyisMQhFhUGpg2xWBgUoTybHxU5xFmSEIHSRyFIGOLMwuD4IZZPjVPWypIH+g+xUD5ctygvUannZAqU/rNyEpKHuFiSFDXEhXnxEcM5gUpdXhSn7D9TGhowXDdEOXtB4d/mlQiUZxU5CWHK2YXD/WdK/Yc9C1OUvYkzg4KHcxKV+TJFgLKWLD9GmZ+ZH6rUC4vjlWcV8oThfEWM8g5zheExQwx/xCMR8eAjBgFwhgfcEQsoMmcoACBwqmymXJKdo+D7y2T5mXyBVOQwiu/s6OQJpKSm8Qc/ifd3QAAgdJjDWr424GkBkDXDWsYroGU5oD4wrFndBNSigLYKUZG8eFCjAgANLKiDBz0YwQzWsIcz3OANPwQjHNFIQComQ4QcFECO6ZiDhShFOVZhHaqxBXXYib04gGYcxUmcxUVcwU3cRxd68Aq9+IB+giAYBIfgEnqEMWFB2BHOhAcxjggmIok4IpVIJ7IJKVFEzCEWE+VEBVFNbCMaiJ+JI8RJ4jxxlbhLPCZeEO+ILySFZJM80pC0JEeTHqQ/GUEmkJPIbHIaOYssIVeQVWQtuYdsIk+SF8mbZBf5iuyjgKJK0aGYUOwpHpRASjQljZJFkVPmUcoolZRayj5KK6WDcp3SRXlN+UylU7lUPtWe6k0NoyZSRdRp1HnUZdRq6k5qE/U09Tr1MbWX+p3GoRnQ7GheNAEthZZNm04rpVXS6mmHaWdoN2k9tA90Ol2HbkV3p4fRU+m59Nn0ZfRN9EZ6G/0qvZvex2Aw9Bh2DB9GNEPIUDBKGRsYexgnGNcYPYxPTFWmMdOZGcJMY0qZi5iVzF3M48xrzGfMfhUNFQsVL5VoFbHKTJWVKttVWlUuq/So9LM0WVYsH1YCK5e1kFXF2sc6w3rAeq+qqmqq6qkaqypRXaBapbpf9ZzqY9XPbC22LTuQPZFdxF7B3sFuY99lv+dwOJYcP04aR8FZwWngnOI84nxS46o5qAnUxGrz1WrUmtSuqb1RV1G3UPdXn6w+S71S/aD6ZfXXGioalhqBGkKNeRo1Gkc0bmv0aXI1nTSjNQs0l2nu0jyv+VyLoWWpFawl1irRqtM6pdXNpXDNuIFcEXcxdzv3DLeHR+dZ8QS8XF45by+vk9erraU9RjtJe4Z2jfYx7S4dio6ljkAnX2elzgGdWzpfRhiO8B+ROWLpiH0jro34qDtS1083U7dMt1H3pu4XPb5esF6e3mq9Zr2H+lR9W/1Y/en6m/XP6L8eyRvpPVI0smzkgZH3DEgDW4M4g9kGdQaXDPoMjQxDDWWGGwxPGb420jHyM8o1Wmt03OiFMdd4nLHEeK3xCeOXfG2+Pz+fX8U/ze81MTAJMyky2WbSadJvamWaaLrItNH0oRnLzMMsy2ytWbtZr7mx+XjzOea7ze9ZqFh4WORYrLfosPhoaWWZbLnEstnyuZWulcBqltVuqwfWHGtf62nWtdY3bOg2HjZ5NptsrtiStq62ObY1tpftSDs3O4ndJruro2ijPEdJR9WOum3Ptve3L7bfbf/YQcch0mGRQ7PDm9Hmo9NGrx7dMfq7o6tjvuN2x/tOWk7hToucWp3eOds6i5xrnG+4cFxCXOa7tLi8HWM3JnPM5jF3XLmu412XuLa7fnNzd5O77XN74W7unu6+0f22B88jxmOZxzlPmmeA53zPo56fvdy8FF4HvP70tvfO897l/Xys1djMsdvHdvuY+gh9tvl0jeOPSx+3dVyXr4mv0LfW94mfmZ/Yr97vmb+Nf67/Hv83AY4B8oDDAR8DvQLnBrYFUYJCg8qCOoO1ghODq4MfhZiGZIfsDukNdQ2dHdoWRguLCFsddltgKBAJGgS94e7hc8NPR7Aj4iOqI55E2kbKI1vHk+PDx68Z/yDKIkoa1RyNaEH0muiHMVYx02J+iaXHxsTWxD6Nc4qbE9cRz42fEr8r/kNCQMLKhPuJ1olFie1J6kkTkxqSPiYHJVckd6WMTpmbcjFVP1WS2pLGSEtKq0/rmxA8Yd2EnomuE0sn3ppkNWnGpPOT9SfnTz42RX2KcMrBdFp6cvqu9K/CaGGtsC9DkLExo1cUKFoveiX2E68Vv8j0yazIfJblk1WR9TzbJ3tN9osc35zKnNeSQEm15G1uWO6W3I950Xk78gbyk/MbC5gF6QVHpFrSPOnpqUZTZ0y9KrOTlcq6pnlNWzetVx4hry8kCicVtih4CpniUpF10Q9Fj4vHFdcUf5qeNP3gDM0Z0hmXZtrOXDrz2ayQWT/Nps4WzW6fYzJn4ZzHc/3nbptHzMuY1z7fbH7J/J4FoQt2LmQtzFv46yLHRRWL/lqcvLi1xLBkQUn3D6E/7C5VK5WX3l7ivWTLj9QfJT92LnVZumHp9zJx2YVyx/LK8q/LRMsuLHdaXrV8YEXWis6Vbis3r6Kvkq66tdp39c4KzYpZFd1rxq9pWstfW7b2r3VT1p2vHFO5ZT1rfdH6rqrIqpYN5htWbfhanVN9syagpnGjwcalGz9uEm+6ttlv874thlvKt3zZKtl6Z1votqZay9rKOnpdcd3T7UnbO37y+KmhXr++vP7bDumOrp1xO083uDc07DLYtXI3ubto94s9E/dc2Ru0t2Wf/b5tjTqN5fuxv2j/y5/Tf751IOJA+0GPg/sOWRzaeJh7uKyJaJrZ1Nuc09zVktpy9Uj4kfZW79bDvzj8suOoydGaY9rHVh5nHS85PnBi1om+Nlnb65PZJ7vbp7TfP5Vy6sbp2NOdZyLOnDsbcvZUh3/HiXM+546e9zp/5ILHheaLbhebLrleOvyr66+HO906my67X2654nml9erYq8ev+V47eT3o+tkbghsXb0bdvHor8dad2xNvd90R33l+N//u23vF9/rvL3hAe1D2UONh5SODR7W/2fzW2OXWdexx0ONLT+Kf3O8Wdb/6vfD3rz0lTzlPK58ZP2t47vz86IuQF1deTnjZ80r2qv916R+af2x8Y/3m0J9+f17qTenteSt/O/Bu2Xu99zv+GvNXe19M36MPBR/6P5Z90vu087PH544vyV+e9U//yvha9c3mW+v3iO8PBgoGBmRCuRAAQAFAZmUB73YAnFSAewVgTRjcqQEAxOB/ADC4g/xvHty7AQBuQB2AZD8gug3YCsBqAcBpA2L8gAQ/kC4uyuffUZjl4jzopdoM0CoHBt4nAwwb4NvtgYH+5oGBb/UA5R7Q9mFwlwcAqgSwrgOxcPN113tn8F/xL2AUEjlEqYDhAAAACXBIWXMAAAsTAAALEwEAmpwYAAAF9mlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDAgNzkuMTYwNDUxLCAyMDE3LzA1LzA2LTAxOjA4OjIxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMTktMTEtMDVUMTU6NTI6MzMrMDI6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMTEtMDVUMTU6NTI6MzMrMDI6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDE5LTExLTA1VDE1OjUyOjMzKzAyOjAwIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjUxMTI1YTgyLWY2YmUtNDY1ZC05NDAyLTk0NWNhZGFjYTkzZSIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjJiNjlhZDExLTI0M2MtMzk0Ny05NDM3LWNiZjkzNTM3ODFiYyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSLine truncated
|
||||
background-repeat: no-repeat, repeat;
|
||||
background-position: right .7em top 50%, 0 0;
|
||||
background-size: .65em auto, 100%;
|
||||
}
|
||||
|
||||
.arrow:after {
|
||||
border-top-color: $lens-menu-hl;
|
||||
}
|
||||
}
|
||||
.popover-body {
|
||||
color: $lens-text-color;
|
||||
|
||||
li.list-group-item {
|
||||
background-color: inherit;
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
||||
a {
|
||||
color: var(--lens-text-color-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
.popover-header{
|
||||
color: $lens-text-color;
|
||||
background-color: $lens-main-bg;
|
||||
border-bottom: 0px;
|
||||
|
||||
i.material-icons {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
#lens-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 70px;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
display: none;
|
||||
|
||||
> iframe {
|
||||
height: calc(100% - var(--lens-bottom-bar-height));
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
// from Lens Dashboard
|
||||
$lens-main-bg: #1e2124 !default; // dark bg
|
||||
$lens-pane-bg: #262b2f !default; // all panels main bg
|
||||
$lens-dock-bg: #2E3136 !default; // terminal and top menu bar
|
||||
$lens-menu-bg: #36393E !default; // sidemenu on left
|
||||
$lens-menu-hl: #414448 !default; // sidemenu on left, top left corner
|
||||
$lens-text-color: #87909c !default;
|
||||
$lens-text-color-light: #a0a0a0 !default;
|
||||
$lens-primary: #3d90ce !default;
|
||||
|
||||
// export as css variables
|
||||
:root {
|
||||
--lens-main-bg: #{$lens-main-bg}; // dark bg
|
||||
--lens-pane-bg: #{$lens-pane-bg}; // all panels main bg
|
||||
--lens-dock-bg: #{$lens-dock-bg}; // terminal and top menu bar
|
||||
--lens-menu-bg: #{$lens-menu-bg}; // sidemenu on left
|
||||
--lens-menu-hl: #{$lens-menu-hl}; // sidemenu on left, top left corner
|
||||
--lens-text-color: #{$lens-text-color};
|
||||
--lens-text-color-light: #{$lens-text-color-light};
|
||||
--lens-primary: #{$lens-primary};
|
||||
--lens-bottom-bar-height: 20px;
|
||||
}
|
||||
|
||||
// Base grayscale colors definitions
|
||||
$white: #fff !default;
|
||||
$gray-100: #f8f9fa !default;
|
||||
$gray-200: #e9ecef !default;
|
||||
$gray-300: #dee2e6 !default;
|
||||
$gray-400: #ced4da !default;
|
||||
$gray-500: #adb5bd !default;
|
||||
$gray-600: #6c757d !default;
|
||||
$gray-700: #495057 !default;
|
||||
$gray-800: #343a40 !default;
|
||||
$gray-900: #1e2124 !default;
|
||||
$black: #000 !default;
|
||||
|
||||
// Base colors definitions
|
||||
$blue: #3d90ce !default;
|
||||
$indigo: #6610f2 !default;
|
||||
$purple: #6f42c1 !default;
|
||||
$pink: #e83e8c !default;
|
||||
$red: #CE3933 !default;
|
||||
$orange: #fd7e14 !default;
|
||||
$yellow: #ffc107 !default;
|
||||
$green: #4caf50 !default;
|
||||
$teal: #20c997 !default;
|
||||
$cyan: #6ca5b7 !default;
|
||||
|
||||
// Theme color default definitions
|
||||
$primary: $lens-primary !default;
|
||||
$secondary: $gray-600 !default;
|
||||
$success: $green !default;
|
||||
$info: $cyan !default;
|
||||
$warning: $yellow !default;
|
||||
$danger: $red !default;
|
||||
$light: $gray-100 !default;
|
||||
$dark: $gray-800 !default;
|
||||
|
||||
// This table defines the theme colors (variant names)
|
||||
$theme-colors: () !default;
|
||||
$theme-colors: map-merge(
|
||||
(
|
||||
'primary': $primary,
|
||||
'secondary': $secondary,
|
||||
'success': $success,
|
||||
'info': $info,
|
||||
'warning': $warning,
|
||||
'danger': $danger,
|
||||
'light': $light,
|
||||
'dark': $dark
|
||||
),
|
||||
$theme-colors
|
||||
);
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 242 KiB |
@@ -1,308 +0,0 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<b-container fluid class="h-100">
|
||||
<b-row align-h="around">
|
||||
<b-col lg="7">
|
||||
<div class="card">
|
||||
<h2>Add Cluster</h2>
|
||||
<div class="add-cluster">
|
||||
<b-form @submit.prevent="doAddCluster">
|
||||
<b-form-group
|
||||
label="Choose config:"
|
||||
>
|
||||
<b-form-file
|
||||
v-model="file"
|
||||
:state="Boolean(file)"
|
||||
placeholder="Choose a file or drop it here..."
|
||||
drop-placeholder="Drop file here..."
|
||||
@input="reloadKubeContexts()"
|
||||
/>
|
||||
|
||||
<div class="mt-3">
|
||||
Selected file: {{ file ? file.name : '' }}
|
||||
</div>
|
||||
|
||||
<b-form-select
|
||||
id="kubecontext-select"
|
||||
v-model="kubecontext"
|
||||
:options="contextNames"
|
||||
@change="onSelect($event)"
|
||||
/>
|
||||
<b-button v-b-toggle.collapse-advanced variant="link">
|
||||
Proxy settings
|
||||
</b-button>
|
||||
</b-form-group>
|
||||
<b-collapse id="collapse-advanced">
|
||||
<b-form-group
|
||||
label="HTTP Proxy server. Used for communicating with Kubernetes API."
|
||||
description="A HTTP proxy server URL (format: http://<address>:<port>)."
|
||||
>
|
||||
<b-form-input
|
||||
v-model="httpsProxy"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-collapse>
|
||||
<b-form-group
|
||||
label="Kubeconfig:"
|
||||
v-if="status === 'ERROR' || kubecontext === 'custom'"
|
||||
>
|
||||
<div class="editor">
|
||||
<prism-editor v-model="clusterconfig" language="yaml" />
|
||||
</div>
|
||||
</b-form-group>
|
||||
<b-alert variant="danger" show v-if="status === 'ERROR'">
|
||||
{{ errorMsg }}
|
||||
<div v-if="errorDetails !== ''">
|
||||
<b-button v-b-toggle.collapse-error variant="link" size="sm">
|
||||
Show details
|
||||
</b-button>
|
||||
<b-collapse id="collapse-error">
|
||||
<code>
|
||||
{{ errorDetails }}
|
||||
</code>
|
||||
</b-collapse>
|
||||
</div>
|
||||
</b-alert>
|
||||
<b-form-row>
|
||||
<b-col>
|
||||
<b-button variant="primary" type="submit" :disabled="clusterconfig === ''">
|
||||
<b-spinner small v-if="isProcessing" label="Small Spinner" />
|
||||
{{ addButtonText }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-form-row>
|
||||
</b-form>
|
||||
</div>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col lg="5" class="help d-none d-lg-block">
|
||||
<h3>Clusters associated with Lens</h3>
|
||||
<p>
|
||||
Add clusters by clicking the <span class="text-primary">Add Cluster</span> button.
|
||||
You'll need to obtain a working kubeconfig for the cluster you want to add.
|
||||
</p>
|
||||
<p>
|
||||
Each <a href="https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#context">cluster context</a> is added as a separate item in the left-side cluster menu to allow you to operate easily on multiple clusters and/or contexts.
|
||||
</p>
|
||||
<p>
|
||||
For more information on kubeconfig see <a href="https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/">Kubernetes docs</a>
|
||||
</p>
|
||||
<p>
|
||||
NOTE: Any manually added cluster is not merged into your kubeconfig file.
|
||||
</p>
|
||||
<p>
|
||||
To see your currently enabled config with <code>kubectl</code>, use <code>kubectl config view --minify --raw</code> command in your terminal.
|
||||
</p>
|
||||
<p>
|
||||
When connecting to a cluster, make sure you have a valid and working kubeconfig for the cluster. Following lists known "gotchas" in some authentication types used in kubeconfig with Lens app.
|
||||
</p>
|
||||
<a href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#option-1-oidc-authenticator">
|
||||
<h4>OIDC (OpenID Connect)</h4>
|
||||
</a>
|
||||
<div>
|
||||
<p>
|
||||
When connecting Lens to OIDC enabled cluster, there's few things you as a user need to take into account.
|
||||
</p>
|
||||
<b>Dedicated refresh token</b>
|
||||
<p>
|
||||
As Lens app utilized kubeconfig is "disconnected" from your main kubeconfig Lens needs to have it's own refresh token it utilizes.
|
||||
If you share the refresh token with e.g. <code>kubectl</code> who ever uses the token first will invalidate it for the next user.
|
||||
One way to achieve this is with <a href="https://github.com/int128/kubelogin">kubelogin</a> tool by removing the tokens (both <code>id_token</code> and <code>refresh_token</code>) from the config and issuing <code>kubelogin</code> command. That'll take you through the login process and will result you having "dedicated" refresh token.
|
||||
</p>
|
||||
</div>
|
||||
<h4>Exec auth plugins</h4>
|
||||
<p>
|
||||
When using <a href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#configuration">exec auth</a> plugins make sure the paths that are used to call any binaries are full paths as Lens app might not be able to call binaries with relative paths. Make also sure that you pass all needed information either as arguments or env variables in the config, Lens app might not have all login shell env variables set automatically.
|
||||
</p>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as PrismEditor from 'vue-prism-editor'
|
||||
import * as k8s from "@kubernetes/client-node"
|
||||
import { dumpConfigYaml } from "../../../main/k8s"
|
||||
import ClustersMixin from "@/_vue/mixins/ClustersMixin";
|
||||
import * as path from "path"
|
||||
import fs from 'fs'
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { writeEmbeddedKubeConfig} from "../../../common/utils/kubeconfig"
|
||||
|
||||
class ClusterAccessError extends Error {}
|
||||
|
||||
export default {
|
||||
name: 'AddClusterPage',
|
||||
mixins: [ClustersMixin],
|
||||
props: { },
|
||||
components: {
|
||||
PrismEditor,
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
file: null,
|
||||
filepath: null,
|
||||
clusterconfig: "",
|
||||
httpsProxy: "",
|
||||
kubecontext: "",
|
||||
status: "",
|
||||
errorMsg: "",
|
||||
errorCluster: "",
|
||||
errorDetails: "",
|
||||
seenContexts: []
|
||||
}
|
||||
},
|
||||
mounted: function() {
|
||||
const kubeConfigPath = path.join(process.env.HOME, '.kube', 'config')
|
||||
this.filepath = kubeConfigPath
|
||||
this.file = new File(fs.readFileSync(this.filepath), this.filepath)
|
||||
this.$store.dispatch("reloadAvailableKubeContexts", this.filepath);
|
||||
this.seenContexts = JSON.parse(JSON.stringify(this.$store.getters.seenContexts)) // clone seenContexts from store
|
||||
this.storeSeenContexts()
|
||||
},
|
||||
computed: {
|
||||
isProcessing: function() {
|
||||
return this.status === "PROCESSING";
|
||||
},
|
||||
addButtonText: function() {
|
||||
if (this.kubecontext === "custom") {
|
||||
return "Add Cluster(s)"
|
||||
} else {
|
||||
return "Add Cluster"
|
||||
}
|
||||
},
|
||||
contextNames: function() {
|
||||
const configs = this.availableContexts
|
||||
const names = configs.map((kc) => {
|
||||
return { text: kc.currentContext + (this.isNewContext(kc.currentContext) ? " (new)": ""), value: dumpConfigYaml(kc) }
|
||||
})
|
||||
names.unshift({text: "Select kubeconfig", value: ""})
|
||||
names.push({text: "Custom ...", value: "custom"})
|
||||
|
||||
return names;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reloadKubeContexts() {
|
||||
this.filepath = this.file.path
|
||||
this.$store.dispatch("reloadAvailableKubeContexts", this.file.path);
|
||||
},
|
||||
isNewContext(context) {
|
||||
return this.newContexts.indexOf(context) > -1
|
||||
},
|
||||
storeSeenContexts() {
|
||||
const configs = this.$store.getters.availableKubeContexts
|
||||
const contexts = configs.map((kc) => {
|
||||
return kc.currentContext
|
||||
})
|
||||
this.$store.dispatch("addSeenContexts", contexts)
|
||||
},
|
||||
onSelect: function() {
|
||||
this.status = "";
|
||||
if (this.kubecontext === "custom") {
|
||||
this.clusterconfig = "";
|
||||
} else {
|
||||
this.clusterconfig = this.kubecontext;
|
||||
}
|
||||
},
|
||||
isOidcAuth: function(authProvider) {
|
||||
if (!authProvider) { return false }
|
||||
if (authProvider.name === "oidc") { return true }
|
||||
|
||||
return false;
|
||||
},
|
||||
doAddCluster: async function() {
|
||||
// Clear previous error details
|
||||
this.errorMsg = ""
|
||||
this.errorCluster = ""
|
||||
this.errorDetails = ""
|
||||
this.status = "PROCESSING"
|
||||
try {
|
||||
const kc = new k8s.KubeConfig();
|
||||
kc.loadFromString(this.clusterconfig); // throws TypeError if we cannot parse kubeconfig
|
||||
const clusterId = uuidv4();
|
||||
// We need to store the kubeconfig to "app-home"/
|
||||
if (this.kubecontext === "custom") {
|
||||
this.filepath = writeEmbeddedKubeConfig(clusterId, this.clusterconfig)
|
||||
}
|
||||
const clusterInfo = {
|
||||
id: clusterId,
|
||||
kubeConfigPath: this.filepath,
|
||||
contextName: kc.currentContext,
|
||||
preferences: {
|
||||
clusterName: kc.currentContext
|
||||
},
|
||||
workspace: this.$store.getters.currentWorkspace.id
|
||||
}
|
||||
if (this.httpsProxy) {
|
||||
clusterInfo.preferences.httpsProxy = this.httpsProxy
|
||||
}
|
||||
console.log("sending clusterInfo:", clusterInfo)
|
||||
let res = await this.$store.dispatch('addCluster', clusterInfo)
|
||||
console.log("addCluster result:", res)
|
||||
if(!res){
|
||||
this.status = "ERROR";
|
||||
return false;
|
||||
}
|
||||
this.status = "SUCCESS"
|
||||
this.$router.push({
|
||||
name: "cluster-page",
|
||||
params: {
|
||||
id: res.id
|
||||
},
|
||||
}).catch((err) => {})
|
||||
} catch (error) {
|
||||
console.log("addCluster raised:", error)
|
||||
if(typeof error === 'string') {
|
||||
this.errorMsg = error;
|
||||
} else if(error instanceof TypeError) {
|
||||
this.errorMsg = "cannot parse kubeconfig";
|
||||
} else if(error.response && error.response.statusCode === 401) {
|
||||
this.errorMsg = "invalid kubeconfig (access denied)"
|
||||
} else if(error.message) {
|
||||
this.errorMsg = error.message
|
||||
} else if(error instanceof ClusterAccessError) {
|
||||
this.errorMsg = `Invalid kubeconfig context ${error.context}`
|
||||
this.errorCluster = error.cluster
|
||||
this.errorDetails = error.details
|
||||
}
|
||||
this.status = "ERROR";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.help{
|
||||
border-left: 1px solid #353a3e;
|
||||
padding-top: 20px;
|
||||
&:first-child{
|
||||
padding-top: 0;
|
||||
}
|
||||
h3{
|
||||
padding: 0.75rem 0 0.75rem 0;
|
||||
}
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
h2{
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.add-cluster {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
padding-left: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,115 +0,0 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<ClosePageButton />
|
||||
<div class="container-fluid lens-workspaces">
|
||||
<div class="header sticky-top">
|
||||
<h2><i class="material-icons">layers</i> Workspaces</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3" />
|
||||
<div class="col-6">
|
||||
<h2>New Workspace</h2>
|
||||
|
||||
<b-form @submit.prevent="createWorkspace">
|
||||
<b-form-group
|
||||
label="Name:"
|
||||
label-for="input-name"
|
||||
>
|
||||
<b-form-input
|
||||
id="input-name"
|
||||
v-model="workspace.name"
|
||||
trim
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
label="Description:"
|
||||
label-for="input-description"
|
||||
>
|
||||
<b-form-input
|
||||
id="input-description"
|
||||
v-model="workspace.description"
|
||||
trim
|
||||
/>
|
||||
</b-form-group>
|
||||
<b-form-row>
|
||||
<b-col>
|
||||
<b-button variant="primary" type="submit">
|
||||
Create Workspace
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-form-row>
|
||||
</b-form>
|
||||
</div>
|
||||
<div class="col-3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { v4 as uuid } from "uuid";
|
||||
import ClosePageButton from "@/_vue/components/common/ClosePageButton";
|
||||
|
||||
export default {
|
||||
name: 'AddWorkspacePage',
|
||||
components: {
|
||||
ClosePageButton
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
workspace: {
|
||||
id: uuid(),
|
||||
name: "",
|
||||
description: ""
|
||||
},
|
||||
errors: {
|
||||
name: null,
|
||||
description: null
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
methods: {
|
||||
createWorkspace: function() {
|
||||
this.$store.commit("addWorkspace", this.workspace)
|
||||
this.$router.push({
|
||||
name: "workspaces-page"
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted: function() {
|
||||
this.$store.commit("hideMenu");
|
||||
},
|
||||
destroyed: function() {
|
||||
this.$store.commit("showMenu");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#app > .main-view > .content {
|
||||
left: 70px;
|
||||
right: 70px;
|
||||
}
|
||||
h2 {
|
||||
i {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
}
|
||||
}
|
||||
.lens-workspaces {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
& input {
|
||||
background-color: #252729 !important;
|
||||
border: 0px !important;
|
||||
color: #87909c !important;
|
||||
}
|
||||
.header {
|
||||
padding-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,88 +0,0 @@
|
||||
<template>
|
||||
<div class="bottom-bar">
|
||||
<div id="workspace-area">
|
||||
<i class="material-icons">layers</i> {{ currentWorkspace.name }}
|
||||
</div>
|
||||
<b-popover target="workspace-area" triggers="click" placement="top" :show.sync="show">
|
||||
<template v-slot:title>
|
||||
<a href="#" @click.prevent="goWorkspaces"><i class="material-icons">layers</i> Workspaces</a>
|
||||
</template>
|
||||
|
||||
<ul
|
||||
v-for="workspace in workspaces"
|
||||
:key="workspace.id"
|
||||
:workspace="workspace"
|
||||
class="list-group list-group-flush"
|
||||
>
|
||||
<li class="list-group-item">
|
||||
<a href="#" @click.prevent="switchWorkspace(workspace)">{{ workspace.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</b-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "BottomBar",
|
||||
data() {
|
||||
return {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentWorkspace: function() {
|
||||
return this.$store.getters.currentWorkspace;
|
||||
},
|
||||
workspaces: function() {
|
||||
return this.$store.getters.workspaces;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
switchWorkspace: function(workspace) {
|
||||
this.show = false;
|
||||
this.$store.commit("setCurrentWorkspace", workspace);
|
||||
this.$store.dispatch("clearClusters");
|
||||
this.$store.dispatch("refreshClusters", workspace);
|
||||
this.$router.push({
|
||||
name: "landing-page"
|
||||
})
|
||||
},
|
||||
goWorkspaces: function() {
|
||||
this.$router.push({
|
||||
name: "workspaces-page"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bottom-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: var(--lens-bottom-bar-height);
|
||||
background-color: var(--lens-primary);
|
||||
z-index: 2000;
|
||||
}
|
||||
#workspace-area {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 10px;
|
||||
display: block;
|
||||
color: #fff;
|
||||
opacity: 0.9;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
&.active{
|
||||
opacity: 1.0;
|
||||
}
|
||||
i {
|
||||
position: relative;
|
||||
top: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,157 +0,0 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<div class="h-100">
|
||||
<div class="wrapper" v-if="status === 'LOADING'">
|
||||
<cube-spinner text="" />
|
||||
<div class="auth-output">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<pre class="auth-output" v-html="authOutput" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrapper" v-if="status === 'ERROR'">
|
||||
<div class="error">
|
||||
<i class="material-icons">{{ error_icon }}</i>
|
||||
<div class="text-center">
|
||||
<h2>{{ cluster.preferences.clusterName }}</h2>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<pre v-html="authOutput" />
|
||||
<b-button variant="link" @click="tryAgain">
|
||||
Reconnect
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CubeSpinner from "@/_vue/components/CubeSpinner";
|
||||
export default {
|
||||
name: "ClusterPage",
|
||||
components: {
|
||||
CubeSpinner
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
authOutput: ""
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
cluster: function() {
|
||||
return this.$store.getters.clusterById(this.$route.params.id);
|
||||
},
|
||||
online: function() {
|
||||
if (!this.cluster) { return false }
|
||||
return this.cluster.online;
|
||||
},
|
||||
accessible: function() {
|
||||
if (!this.cluster) { return false }
|
||||
return this.cluster.accessible;
|
||||
},
|
||||
lens: function() {
|
||||
return this.$store.getters.lensById(this.cluster.id);
|
||||
},
|
||||
status: function() {
|
||||
if (this.cluster) {
|
||||
if (this.cluster.accessible && this.lens.loaded === true) {
|
||||
return "SUCCESS";
|
||||
} else if (this.cluster.accessible === false) {
|
||||
return "ERROR";
|
||||
}
|
||||
return "LOADING";
|
||||
}
|
||||
return "ERROR";
|
||||
},
|
||||
error_icon: function() {
|
||||
if (!this.cluster.online) {
|
||||
return "cloud_off"
|
||||
} else {
|
||||
return "https"
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tryAgain: function() {
|
||||
this.authOutput = ""
|
||||
this.cluster.accessible = null
|
||||
setTimeout(() => {
|
||||
this.loadLens()
|
||||
}, 1000)
|
||||
|
||||
},
|
||||
loadLens: function() {
|
||||
this.authOutput = "Connecting ...\n";
|
||||
this.$promiseIpc.on(`kube-auth:${this.cluster.id}`, (output) => {
|
||||
this.authOutput += output.data;
|
||||
})
|
||||
this.toggleLens();
|
||||
return this.$store.dispatch("refineCluster", this.$route.params.id);
|
||||
},
|
||||
lensLoaded: function() {
|
||||
console.log("lens loaded")
|
||||
this.lens.loaded = true;
|
||||
this.$store.commit("updateLens", this.lens);
|
||||
},
|
||||
// Called only when online state changes
|
||||
toggleLens: function() {
|
||||
if (!this.lens) { return }
|
||||
if (this.accessible) {
|
||||
setTimeout(this.activateLens, 0); // see: https://github.com/electron/electron/issues/10016
|
||||
} else {
|
||||
this.hideLens();
|
||||
}
|
||||
},
|
||||
activateLens: async function() {
|
||||
console.log("activate lens")
|
||||
if (!this.lens.webview) {
|
||||
console.log("creating an iframe")
|
||||
const webview = document.createElement('iframe');
|
||||
webview.addEventListener('load', this.lensLoaded);
|
||||
webview.src = this.cluster.url;
|
||||
this.lens.webview = webview;
|
||||
}
|
||||
this.$store.dispatch("attachWebview", this.lens);
|
||||
this.$tracker.event("cluster", "open");
|
||||
},
|
||||
hideLens: function() {
|
||||
this.$store.dispatch("hideWebviews");
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadLens();
|
||||
},
|
||||
destroyed() {
|
||||
this.hideLens();
|
||||
},
|
||||
watch: {
|
||||
"$route": "loadLens",
|
||||
"online": "toggleLens",
|
||||
"cluster": "toggleLens",
|
||||
"accessible": function(newStatus, oldStatus) {
|
||||
console.log("accessible watch, vals:", newStatus, oldStatus);
|
||||
if(newStatus === false) { // accessble == false
|
||||
this.$tracker.event("cluster", "open-failed");
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
div.auth-output {
|
||||
padding-top: 250px;
|
||||
width: 70%;
|
||||
pre {
|
||||
height: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.error {
|
||||
width: 90%;
|
||||
}
|
||||
pre {
|
||||
font-size: 80%;
|
||||
overflow: auto;
|
||||
max-height: 150px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,141 +0,0 @@
|
||||
<template>
|
||||
<div class="settings-section">
|
||||
<b>Metrics</b>
|
||||
<p class="description">
|
||||
Enable timeseries data visualization (Prometheus stack) for your cluster. Install this only if you don't have existing Prometheus stack installed. You can see preview of manifests <a href="https://github.com/lensapp/lens/tree/master/src/features/metrics">here</a>.
|
||||
</p>
|
||||
<div class="actions">
|
||||
<b-button @click="install" v-if="!settings.installed" :disabled="!cluster.isAdmin || isProcessing || !canInstall" variant="primary">
|
||||
<b-spinner small v-if="isProcessing" label="Small Spinner" />
|
||||
Install
|
||||
</b-button>
|
||||
<b-button @click="upgrade" v-if="isUpgradeAvailable" :disabled="!cluster.isAdmin || isProcessing || isUpgrading" variant="primary">
|
||||
<b-spinner small v-if="isUpgrading" label="Small Spinner" />
|
||||
Upgrade
|
||||
</b-button>
|
||||
<b-button @click="uninstall" v-if="settings.installed" :disabled="!cluster.isAdmin || isProcessing || isUpgrading" variant="danger">
|
||||
<b-spinner small v-if="isProcessing" label="Small Spinner" />
|
||||
Uninstall
|
||||
</b-button>
|
||||
<b-alert show variant="danger" v-if="status === 'ERROR'">
|
||||
{{ errorMsg }}
|
||||
</b-alert>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const semver = require('semver');
|
||||
|
||||
export default {
|
||||
name: 'Metrics',
|
||||
components: {},
|
||||
props: {
|
||||
cluster: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: null
|
||||
},
|
||||
feature: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: null
|
||||
},
|
||||
settings: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: Object
|
||||
}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
status: "",
|
||||
errorMsg: "",
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
isUpgradeAvailable: function() {
|
||||
return this.cluster.features.metrics.canUpgrade;
|
||||
},
|
||||
isProcessing: function() {
|
||||
return this.status === "PROCESSING";
|
||||
},
|
||||
isUpgrading: function() {
|
||||
return this.status === "UPGRADING";
|
||||
},
|
||||
canInstall: function() {
|
||||
return !this.cluster.preferences.prometheus
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
install: async function(){
|
||||
this.status="PROCESSING";
|
||||
let error = null;
|
||||
let result = await this.$store.dispatch("installClusterFeature", {
|
||||
name: this.feature,
|
||||
clusterId: this.cluster.id,
|
||||
config: null,
|
||||
}).catch(e => {
|
||||
error = e;
|
||||
return false; // just returning false to promise
|
||||
})
|
||||
|
||||
// handle exceptions here; but oh why, why do we have to???
|
||||
if(error){
|
||||
this.status = "ERROR";
|
||||
this.errorMsg = error;
|
||||
return false;
|
||||
}
|
||||
|
||||
// now let's look at result...
|
||||
if(result.success === false) {
|
||||
this.status = "ERROR"
|
||||
this.errorMsg = result.message
|
||||
} else {
|
||||
this.$store.dispatch("refineCluster", this.cluster.id);
|
||||
this.status = "";
|
||||
this.errorMsg = "";
|
||||
}
|
||||
},
|
||||
uninstall: async function(){
|
||||
this.status = "PROCESSING";
|
||||
let error = null;
|
||||
try {
|
||||
let result = await this.$store.dispatch("uninstallClusterFeature", {
|
||||
name: this.feature,
|
||||
clusterId: this.cluster.id,
|
||||
})
|
||||
console.log("uninstall result:", result);
|
||||
this.$store.dispatch("refineCluster", this.cluster.id);
|
||||
this.status = "SUCCESS";
|
||||
this.errorMsg = "";
|
||||
} catch(error) {
|
||||
this.status = "ERROR"
|
||||
this.errorMsg = error
|
||||
console.log("Error uninstalling:", error);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
upgrade: async function(){
|
||||
this.status = "UPGRADING";
|
||||
try {
|
||||
let result = await this.$store.dispatch("upgradeClusterFeature", {
|
||||
name: this.feature,
|
||||
clusterId: this.cluster.id,
|
||||
config: null,
|
||||
})
|
||||
this.$store.dispatch("refineCluster", this.cluster.id);
|
||||
this.status = "";
|
||||
this.errorMsg = "";
|
||||
} catch(error) {
|
||||
this.status = "ERROR"
|
||||
this.errorMsg = error.message
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@@ -1,128 +0,0 @@
|
||||
<template>
|
||||
<div class="settings-section">
|
||||
<b>User Mode</b>
|
||||
<p class="description">
|
||||
User Mode feature enables non-admin users to see namespaces they have access to. This is achieved by configuring RBAC rules so that every authenticated user is granted to list namespaces.
|
||||
</p>
|
||||
<div class="actions">
|
||||
<b-button @click="install" v-if="!settings.installed" :disabled="!cluster.isAdmin || isProcessing" variant="primary">
|
||||
<b-spinner small v-if="isProcessing" label="Small Spinner" />
|
||||
Install
|
||||
</b-button>
|
||||
<b-button @click="uninstall" v-if="settings.installed" :disabled="!cluster.isAdmin || isProcessing" variant="danger">
|
||||
<b-spinner small v-if="isProcessing" label="Small Spinner" />
|
||||
Uninstall
|
||||
</b-button>
|
||||
<b-button @click="upgrade" v-if="isUpgradeAvailable" :disabled="!cluster.isAdmin" variant="primary">
|
||||
Upgrade
|
||||
</b-button>
|
||||
<b-alert show variant="danger" v-if="status === 'ERROR'">
|
||||
{{ errorMsg }}
|
||||
</b-alert>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const semver = require('semver');
|
||||
|
||||
export default {
|
||||
name: 'UserMode',
|
||||
components: {},
|
||||
props: {
|
||||
cluster: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: null
|
||||
},
|
||||
feature: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: null
|
||||
},
|
||||
settings: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: Object
|
||||
}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
status: "",
|
||||
errorMsg: "",
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
isProcessing: function() {
|
||||
return this.status === "PROCESSING";
|
||||
},
|
||||
isUpgradeAvailable: function() {
|
||||
if(!this.settings.installed) return false;
|
||||
if(!this.settings.currentVersion) return false;
|
||||
if(!this.settings.latestVersion) return false;
|
||||
let currentVersion = (this.settings.currentVersion.charAt(0) === "v") ? this.settings.currentVersion.substr(1) : this.settings.currentVersion;
|
||||
let latestVersion = (this.settings.latestVersion.charAt(0) === "v") ? this.settings.latestVersion.substr(1) : this.settings.latestVersion;
|
||||
return semver.gt(latestVersion, currentVersion)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
install: async function(){
|
||||
this.status="PROCESSING";
|
||||
let error = null;
|
||||
let result = await this.$store.dispatch("installClusterFeature", {
|
||||
name: this.feature,
|
||||
clusterId: this.cluster.id,
|
||||
config: null,
|
||||
}).catch(e => {
|
||||
error = e;
|
||||
return false; // just returning false to promise
|
||||
})
|
||||
|
||||
// handle exceptions here; but oh why, why do we have to???
|
||||
if(error){
|
||||
this.status = "ERROR";
|
||||
this.errorMsg = error;
|
||||
return false;
|
||||
}
|
||||
|
||||
// now let's look at result...
|
||||
if(result.success === false) {
|
||||
this.status = "ERROR"
|
||||
this.errorMsg = result.message
|
||||
} else {
|
||||
this.$store.dispatch("refineCluster", this.cluster.id);
|
||||
this.status = "";
|
||||
this.errorMsg = "";
|
||||
}
|
||||
},
|
||||
uninstall: async function(){
|
||||
this.status="PROCESSING";
|
||||
let error = null;
|
||||
try {
|
||||
let result = await this.$store.dispatch("uninstallClusterFeature", {
|
||||
name: this.feature,
|
||||
clusterId: this.cluster.id,
|
||||
})
|
||||
console.log("uninstall result:", result);
|
||||
this.$store.dispatch("refineCluster", this.cluster.id);
|
||||
this.status="SUCCESS";
|
||||
this.errorMsg = "";
|
||||
} catch(error) {
|
||||
this.status = "ERROR"
|
||||
this.errorMsg = error
|
||||
console.log("Error uninstalling:", error);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
},
|
||||
upgrade: async function(){
|
||||
// todo
|
||||
return true;
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@@ -1,7 +0,0 @@
|
||||
import Metrics from './Metrics'
|
||||
import UserMode from './UserMode'
|
||||
|
||||
export default {
|
||||
Metrics,
|
||||
UserMode
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<template>
|
||||
<div id="cluster-features" class="row">
|
||||
<div class="col-12">
|
||||
<h2>Features</h2>
|
||||
<component
|
||||
v-for="featureName in features"
|
||||
:key="featureName"
|
||||
:is="featureName"
|
||||
:cluster="cluster"
|
||||
:feature="featureName"
|
||||
:settings="feature(featureName)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// register all sub components
|
||||
import components from './Components'
|
||||
|
||||
export default {
|
||||
name: 'ClusterFeatures',
|
||||
components: components,
|
||||
props: {
|
||||
cluster: {
|
||||
type: Object,
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
features: function() {
|
||||
if (!this.cluster) return []
|
||||
if (!this.cluster.features) return []
|
||||
|
||||
return Object.keys(this.cluster.features)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
feature: function( name ){ return this.cluster.features[name] }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
h2{
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,148 +0,0 @@
|
||||
<template>
|
||||
<div class="settings-section">
|
||||
<b>Cluster Icon</b>
|
||||
<p>
|
||||
Define cluster icon. By default automatically generated.
|
||||
</p>
|
||||
<div class="row">
|
||||
<span class="cluster-settings-icon">
|
||||
<img v-if="preferences.icon" :src="preferences.icon" class="cluster-icon">
|
||||
<hashicon v-else :name="cluster.preferences.clusterName" size="38" />
|
||||
</span>
|
||||
<div class="col">
|
||||
<b-form-file
|
||||
ref="fileUpload"
|
||||
v-model="file"
|
||||
accept="image/jpeg, image/png, image/gif"
|
||||
:state="Boolean(file)"
|
||||
placeholder="Browse for new icon..."
|
||||
drop-placeholder="Drop file here..."
|
||||
browse-text=""
|
||||
@input="upload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<a href="#" @click.prevent="reset" v-if="preferences.icon" class="text-muted">
|
||||
Use automatically generated icon
|
||||
</a>
|
||||
<b-alert show variant="danger" v-if="status === 'ERROR'">
|
||||
{{ errorMsg }}
|
||||
</b-alert>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import hashicon from "../../hashicon/hashicon";
|
||||
export default {
|
||||
name: 'ClusterIcon',
|
||||
components: {
|
||||
hashicon
|
||||
},
|
||||
props: {
|
||||
cluster: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
status: "",
|
||||
errorMsg: "",
|
||||
file: null,
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
preferences: function() {
|
||||
return this.cluster.preferences;
|
||||
},
|
||||
isProcessing: function() {
|
||||
return this.status === "PROCESSING";
|
||||
},
|
||||
isResetProcessing: function() {
|
||||
return this.resetStatus === "PROCESSING";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reset: async function(){
|
||||
this.resetStatus = "PROCESSING";
|
||||
let error = null;
|
||||
|
||||
let result = await this.$store.dispatch("resetClusterIcon", {
|
||||
clusterId: this.cluster.id
|
||||
}).catch(e => {
|
||||
error = e;
|
||||
return false; // just returning false to promise
|
||||
})
|
||||
|
||||
// handle exceptions here; but oh why, why do we have to???
|
||||
if(error){
|
||||
this.resetStatus = "ERROR";
|
||||
this.errorMsg = error;
|
||||
return false;
|
||||
}
|
||||
|
||||
// now let's look at result...
|
||||
if(result.success === false) {
|
||||
this.resetStatus = "ERROR"
|
||||
this.errorMsg = result.message
|
||||
} else {
|
||||
this.resetStatus = "";
|
||||
this.errorMsg = "";
|
||||
}
|
||||
},
|
||||
upload: async function(file){
|
||||
if(!file) {
|
||||
return
|
||||
}
|
||||
this.status="PROCESSING";
|
||||
let error = null;
|
||||
|
||||
let result = await this.$store.dispatch("uploadClusterIcon", {
|
||||
clusterId: this.cluster.id,
|
||||
name: file.name,
|
||||
path: file.path
|
||||
}).catch(e => {
|
||||
error = e;
|
||||
return false; // just returning false to promise
|
||||
})
|
||||
// handle exceptions here; but oh why, why do we have to???
|
||||
if(error){
|
||||
this.status = "ERROR";
|
||||
this.errorMsg = error;
|
||||
return false;
|
||||
}
|
||||
|
||||
// now let's look at result...
|
||||
if(result.success === false) {
|
||||
this.status = "ERROR"
|
||||
this.errorMsg = result.message
|
||||
} else {
|
||||
this.status = "";
|
||||
this.errorMsg = "";
|
||||
this.file = null;
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
mounted: function(){
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.description{
|
||||
padding: 0.75rem;
|
||||
}
|
||||
.actions{
|
||||
border-top: 1px solid rgba(255,255,255,0.10);
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.cluster-settings-icon {
|
||||
background: #252729;
|
||||
margin-left: 15px;
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,49 +0,0 @@
|
||||
<template>
|
||||
<div class="settings-section">
|
||||
<b>Cluster Name</b>
|
||||
<p>
|
||||
Define cluster name.
|
||||
</p>
|
||||
<b-form-input v-model="cluster.preferences.clusterName"
|
||||
:placeholder="cluster.preferences.clusterName"
|
||||
:state="errors.name"
|
||||
trim
|
||||
@blur="onSave"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ClusterName',
|
||||
props: {
|
||||
cluster: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
errors: {
|
||||
name: null
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSave: function() {
|
||||
if(this.cluster.preferences.clusterName === "") {
|
||||
this.errors.name = false
|
||||
return
|
||||
} else {
|
||||
this.errors.name = true
|
||||
}
|
||||
this.$store.dispatch("storeCluster", this.cluster);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -1,52 +0,0 @@
|
||||
<template>
|
||||
<div class="settings-section">
|
||||
<b>Cluster Workspace</b>
|
||||
<p>
|
||||
Define cluster <a href="/#/workspaces">workspace</a>.
|
||||
</p>
|
||||
<b-form-select
|
||||
v-model="cluster.workspace"
|
||||
:options="workspaces"
|
||||
@change="onSave"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ClusterWorkspace',
|
||||
props: {
|
||||
cluster: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
workspaces: function() {
|
||||
return this.$store.getters.workspaces.map((ws) => {
|
||||
return {
|
||||
value: ws.id,
|
||||
text: ws.name
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSave: async function(workspaceId) {
|
||||
this.cluster.workspace = workspaceId
|
||||
await this.$store.dispatch("storeCluster", this.cluster);
|
||||
const ws = this.$store.getters.workspaceById(this.cluster.workspace);
|
||||
if (ws) {
|
||||
this.$store.commit("setCurrentWorkspace", ws);
|
||||
await this.$store.dispatch("clearClusters");
|
||||
await this.$store.dispatch("refreshClusters", ws);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -1,35 +0,0 @@
|
||||
<template>
|
||||
<div id="cluster-general" class="row">
|
||||
<div class="col-12">
|
||||
<h2>General</h2>
|
||||
<cluster-name :cluster="cluster" />
|
||||
<cluster-workspace :cluster="cluster" />
|
||||
<cluster-icon :cluster="cluster" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClusterIcon from "./ClusterIcon";
|
||||
import ClusterName from "./ClusterName"
|
||||
import ClusterWorkspace from "./ClusterWorkspace"
|
||||
export default {
|
||||
name: "ClusterSettingsGeneral",
|
||||
props: {
|
||||
cluster: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ClusterIcon,
|
||||
ClusterName,
|
||||
ClusterWorkspace
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -1,69 +0,0 @@
|
||||
<template>
|
||||
<div id="cluster-status" class="cluster-settings-overview row">
|
||||
<div class="col-12">
|
||||
<h2>Status</h2>
|
||||
<div class="settings-section">
|
||||
<b>Cluster Status</b>
|
||||
<p>
|
||||
Cluster status information including detected distribution, kernel version and online status
|
||||
</p>
|
||||
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
Online Status
|
||||
</th>
|
||||
<td>{{ cluster.online ? "online" : "offline (" + (cluster.failureReason || "unknown reason") + ")" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
Distribution
|
||||
</th>
|
||||
<td>{{ cluster.distribution }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
Kernel Version
|
||||
</th>
|
||||
<td>{{ cluster.version }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
API Address
|
||||
</th>
|
||||
<td>{{ cluster.apiUrl }}</td>
|
||||
</tr>
|
||||
<tr v-if="cluster.nodes">
|
||||
<th scope="row">
|
||||
Nodes Count
|
||||
</th>
|
||||
<td>{{ cluster.nodes }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ClusterSettingsOverview',
|
||||
props: {
|
||||
cluster: {
|
||||
type: Object,
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
@@ -1,196 +0,0 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="cluster-settings-section">
|
||||
<b>HTTP Proxy</b>
|
||||
|
||||
<b-form-group
|
||||
label="HTTP Proxy server. Used for communicating with Kubernetes API."
|
||||
description="A HTTP proxy server URL (format: http://<address>:<port>)."
|
||||
>
|
||||
<b-form-input
|
||||
v-model="cluster.preferences.httpsProxy"
|
||||
id="input-httpsproxy"
|
||||
@blur="onHttpsProxySave"
|
||||
/>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="cluster-settings-section">
|
||||
<b>Prometheus</b>
|
||||
<p>Use pre-installed Prometheus service for metrics. Please refer to the <a href="https://github.com/lensapp/lens/blob/master/troubleshooting/custom-prometheus.md">guide</a> for possible configuration changes.</p>
|
||||
|
||||
<b-form-group
|
||||
label="Prometheus installation method."
|
||||
description="What query format is used to fetch metrics from Prometheus"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="prometheusProvider"
|
||||
:options="prometheusProviders"
|
||||
@change="onPrometheusProviderSave"
|
||||
/>
|
||||
</b-form-group>
|
||||
<b-form-group
|
||||
label="Prometheus service address."
|
||||
description="An address to an existing Prometheus installation (<namespace>/<service>:<port>). Lens tries to auto-detect address if left empty."
|
||||
v-if="canEditPrometheusPath"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="prometheusPath"
|
||||
placeholder="<namespace>/<service>:<port>"
|
||||
id="input-prometheuspath"
|
||||
@blur="onPrometheusSave"
|
||||
:state="errors.prometheusPath"
|
||||
/>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="cluster-settings-section">
|
||||
<b>Working Directory</b>
|
||||
|
||||
<b-form-group
|
||||
label="Terminal working directory."
|
||||
description="An explicit start path where the terminal will be launched, this is used as the current working directory (cwd) for the shell process."
|
||||
>
|
||||
<b-form-input
|
||||
v-model="cluster.preferences.terminalCWD"
|
||||
placeholder="$HOME"
|
||||
id="input-terminalcwd"
|
||||
@blur="onTerminalCwdSave"
|
||||
:state="errors.terminalcwd"
|
||||
:formatter="expandPath"
|
||||
/>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { lstatSync } from "fs"
|
||||
import { prometheusProviders } from '../../../../../common/prometheus-providers';
|
||||
|
||||
export default {
|
||||
name: 'ClusterSettingsPreferences',
|
||||
props: {
|
||||
cluster: {
|
||||
type: Object,
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
errors: {
|
||||
prometheusPath: null,
|
||||
terminalcwd: null
|
||||
},
|
||||
prometheusPath: "",
|
||||
prometheusProvider: "",
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
prometheusProviders: function() {
|
||||
const providers = prometheusProviders.map((provider) => {
|
||||
return { text: provider.name, value: provider.id }
|
||||
})
|
||||
providers.unshift({text: "Auto detect", value: ""})
|
||||
|
||||
return providers;
|
||||
},
|
||||
canEditPrometheusPath: function() {
|
||||
if (this.prometheusProvider === "") return false
|
||||
if (this.prometheusProvider === "lens") return false
|
||||
|
||||
return true
|
||||
}
|
||||
},
|
||||
mounted: async function() {
|
||||
this.updateValues()
|
||||
},
|
||||
methods: {
|
||||
updateValues: function(){
|
||||
if (this.cluster.preferences.prometheus) {
|
||||
const prom = this.cluster.preferences.prometheus
|
||||
if (!prom.prefix) prom.prefix = ""
|
||||
this.prometheusPath = `${prom.namespace}/${prom.service}:${prom.port}${prom.prefix}`
|
||||
} else {
|
||||
this.prometheusPath = ""
|
||||
}
|
||||
if (this.cluster.preferences.prometheusProvider) {
|
||||
this.prometheusProvider = this.cluster.preferences.prometheusProvider.type
|
||||
} else {
|
||||
this.prometheusProvider = ""
|
||||
}
|
||||
},
|
||||
parsePrometheusPath: function(path) {
|
||||
const parsed = path.split(/\/|:/, 3)
|
||||
const apiPrefix = path.substring(parsed.join("/").length)
|
||||
if (parsed[0] && parsed[1] && parsed[2]) {
|
||||
this.errors.prometheusPath = true;
|
||||
} else {
|
||||
this.errors.prometheusPath = false;
|
||||
}
|
||||
return {
|
||||
namespace: parsed[0],
|
||||
service: parsed[1],
|
||||
port: parsed[2],
|
||||
prefix: apiPrefix
|
||||
}
|
||||
},
|
||||
expandPath: function(value, event) {
|
||||
if(value === "") {
|
||||
this.errors.terminalcwd = null
|
||||
return value;
|
||||
}
|
||||
if(value.substr(0, 1) == "~") {
|
||||
value = process.env.HOME + value.substr(1);
|
||||
} else if(value.substr(0, 5) == "$HOME") {
|
||||
value = process.env.HOME + value.substr(5);
|
||||
}
|
||||
|
||||
try {
|
||||
this.errors.terminalcwd = lstatSync(value).isDirectory()
|
||||
} catch(_err) {
|
||||
this.errors.terminalcwd = false
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
onHttpsProxySave: function() {
|
||||
if(this.cluster.preferences.httpsProxy === "") this.cluster.preferences.httpsProxy = null
|
||||
this.$store.dispatch("storeCluster", this.cluster);
|
||||
},
|
||||
onPrometheusSave: function() {
|
||||
if (this.prometheusPath === "") {
|
||||
this.cluster.preferences.prometheus = null;
|
||||
} else {
|
||||
this.cluster.preferences.prometheus = this.parsePrometheusPath(this.prometheusPath);
|
||||
}
|
||||
this.$store.dispatch("storeCluster", this.cluster);
|
||||
},
|
||||
onPrometheusProviderSave: function() {
|
||||
if (this.prometheusProvider === "") {
|
||||
this.cluster.preferences.prometheusProvider = null;
|
||||
this.cluster.preferences.prometheus = null;
|
||||
} else {
|
||||
this.cluster.preferences.prometheusProvider = { type: this.prometheusProvider }
|
||||
}
|
||||
this.$store.dispatch("storeCluster", this.cluster);
|
||||
},
|
||||
onTerminalCwdSave: function() {
|
||||
if(this.cluster.preferences.terminalCWD === "") this.cluster.preferences.terminalCWD = null
|
||||
this.$store.dispatch("storeCluster", this.cluster);
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
"cluster": "updateValues",
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -1,185 +0,0 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<ClosePageButton />
|
||||
<div class="container-fluid settings">
|
||||
<div v-if="cluster" class="header sticky-top">
|
||||
<h2>
|
||||
<div class="icon d-inline">
|
||||
<img v-if="preferences.icon" :src="preferences.icon" class="cluster-icon">
|
||||
<hashicon v-else :name="cluster.preferences.clusterName" size="30" />
|
||||
</div>
|
||||
{{ cluster.preferences.clusterName }}
|
||||
</h2>
|
||||
</div>
|
||||
<div v-if="cluster" class="row">
|
||||
<div class="col-3" />
|
||||
<div ref="content" id="cluster-settings" class="col-6">
|
||||
<cluster-settings-overview :cluster="cluster" />
|
||||
<cluster-settings-general :cluster="cluster" />
|
||||
<cluster-settings-preferences :cluster="cluster" />
|
||||
<cluster-settings-features :cluster="cluster" />
|
||||
|
||||
<div id="cluster-remove" class="settings-section">
|
||||
<h2>Remove Cluster</h2>
|
||||
<b-button v-b-modal.bv-modal-confirm variant="danger" type="submit">
|
||||
Remove Cluster
|
||||
</b-button>
|
||||
<!-- Modal confirmation -->
|
||||
<b-modal id="bv-modal-confirm" @ok="removeCluster" ok-title="Remove" ok-variant="danger" title-class="confirm-header" hide-backdrop title="Confirm cluster delete">
|
||||
<p>Are you sure you want to delete <strong>{{ preferences.clusterName }}</strong> cluster from Lens?</p>
|
||||
</b-modal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!cluster">
|
||||
<cube-spinner text="Loading..." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CubeSpinner from "@/_vue/components/CubeSpinner";
|
||||
import ClusterSettingsGeneral from "./General/index";
|
||||
import ClusterSettingsOverview from "./Overview/index";
|
||||
import ClusterSettingsPreferences from "./Preferences/index";
|
||||
import ClusterSettingsFeatures from "./Features/index";
|
||||
import hashicon from "../hashicon/hashicon";
|
||||
import ClosePageButton from "@/_vue/components/common/ClosePageButton";
|
||||
|
||||
export default {
|
||||
name: 'ClusterSettingsPage',
|
||||
components: {
|
||||
CubeSpinner,
|
||||
ClosePageButton,
|
||||
ClusterSettingsGeneral,
|
||||
ClusterSettingsOverview,
|
||||
ClusterSettingsPreferences,
|
||||
ClusterSettingsFeatures,
|
||||
hashicon
|
||||
},
|
||||
computed: {
|
||||
cluster: function() {
|
||||
return this.$store.getters.clusterById(this.$route.params.id);
|
||||
},
|
||||
preferences: function() {
|
||||
return this.cluster.preferences;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadLens: function() {
|
||||
this.$store.dispatch("refineCluster", this.cluster.id);
|
||||
},
|
||||
removeCluster: async function(){
|
||||
let res = await this.$store.dispatch('removeCluster', this.cluster.id);
|
||||
if(!res) return false;
|
||||
|
||||
this.$router.push({
|
||||
name: "landing-page"
|
||||
}).catch(err => {})
|
||||
return true;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadLens()
|
||||
},
|
||||
watch: {
|
||||
"$route": "loadLens"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.header {
|
||||
& h2 {
|
||||
padding: 0.5rem
|
||||
}
|
||||
padding-top: 5px;
|
||||
padding-left: 15px;
|
||||
border-bottom: 1px solid rgb(53, 58, 62);
|
||||
background-color: #1e2124;
|
||||
& img {
|
||||
margin-right: 10px;
|
||||
height: 35px;
|
||||
}
|
||||
.hashicon {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
margin-right: 10px;
|
||||
& canvas{
|
||||
position: relative;
|
||||
bottom: -6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.settings-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#cluster-settings {
|
||||
& input {
|
||||
background-color: #252729 !important;
|
||||
border: 0px !important;
|
||||
color: #87909c !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cluster-icon {
|
||||
width: 38px;
|
||||
}
|
||||
|
||||
h2{
|
||||
padding: 0.75rem;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.table th, .table td{
|
||||
padding-bottom: 0px !important;
|
||||
}
|
||||
|
||||
.custom-select {
|
||||
-webkit-appearance: inherit;
|
||||
}
|
||||
|
||||
.collapsed > .when-opened, :not(.collapsed) > .when-closed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.confirm-header{
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding: 0px !important;
|
||||
&.active {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-file-label{
|
||||
background: transparent !important;
|
||||
color: #3d90ce !important;
|
||||
}
|
||||
|
||||
.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label {
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.custom-file-label::after {
|
||||
content: "" !important;
|
||||
width: 0px;
|
||||
padding: 0px !important;
|
||||
background-color: transparent !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,116 +0,0 @@
|
||||
<template>
|
||||
<div class="loading">
|
||||
<div class="CubeSpinner">
|
||||
<p>{{ text }}</p>
|
||||
<div class="sk-cube-grid">
|
||||
<div class="sk-cube sk-cube1" />
|
||||
<div class="sk-cube sk-cube2" />
|
||||
<div class="sk-cube sk-cube3" />
|
||||
<div class="sk-cube sk-cube4" />
|
||||
<div class="sk-cube sk-cube5" />
|
||||
<div class="sk-cube sk-cube6" />
|
||||
<div class="sk-cube sk-cube7" />
|
||||
<div class="sk-cube sk-cube8" />
|
||||
<div class="sk-cube sk-cube9" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CubeSpinner',
|
||||
components: {},
|
||||
methods: {},
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "Connecting ..."
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.loading {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin: -44px;
|
||||
margin-top: -70px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* http://tobiasahlin.com/spinkit/ */
|
||||
.CubeSpinner {
|
||||
--size: 70px;
|
||||
.sk-cube-grid {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
transform: rotate(45deg);
|
||||
margin: 24px auto;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sk-cube-grid .sk-cube {
|
||||
width: 33%;
|
||||
height: 33%;
|
||||
float: left;
|
||||
-webkit-animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out;
|
||||
animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out;
|
||||
}
|
||||
.sk-cube-grid .sk-cube1 {
|
||||
-webkit-animation-delay: 0.2s;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube2 {
|
||||
-webkit-animation-delay: 0.3s;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube3 {
|
||||
-webkit-animation-delay: 0.4s;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube4 {
|
||||
-webkit-animation-delay: 0.1s;
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube5 {
|
||||
-webkit-animation-delay: 0.2s;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube6 {
|
||||
-webkit-animation-delay: 0.3s;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube7 {
|
||||
-webkit-animation-delay: 0s;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube8 {
|
||||
-webkit-animation-delay: 0.1s;
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube9 {
|
||||
-webkit-animation-delay: 0.2s;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
@-webkit-keyframes sk-cubeGridScaleDelay {
|
||||
0%, 35% {
|
||||
background-color: #36393e;
|
||||
}
|
||||
0%, 70%, 100% {
|
||||
-webkit-transform: scale3D(1, 1, 1);
|
||||
transform: scale3D(1, 1, 1);
|
||||
}
|
||||
35% {
|
||||
-webkit-transform: scale3D(0, 0, 1);
|
||||
transform: scale3D(0, 0, 1);
|
||||
}
|
||||
100% {
|
||||
background-color: #3d90ce;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,124 +0,0 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<ClosePageButton />
|
||||
<div class="container-fluid lens-workspaces">
|
||||
<div class="header sticky-top">
|
||||
<h2><i class="material-icons">layers</i> Workspaces</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3" />
|
||||
<div class="col-6">
|
||||
<h2>Edit Workspace</h2>
|
||||
|
||||
<b-form @submit.prevent="updateWorkspace">
|
||||
<b-form-group
|
||||
label="Name:"
|
||||
label-for="input-name"
|
||||
>
|
||||
<b-form-input
|
||||
id="input-name"
|
||||
v-model="workspace.name"
|
||||
trim
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
label="Description:"
|
||||
label-for="input-description"
|
||||
>
|
||||
<b-form-input
|
||||
id="input-description"
|
||||
v-model="workspace.description"
|
||||
trim
|
||||
/>
|
||||
</b-form-group>
|
||||
<b-form-row>
|
||||
<b-col>
|
||||
<b-button variant="primary" type="submit">
|
||||
Save
|
||||
</b-button>
|
||||
<b-button v-if="workspace.id !== 'default'" v-b-modal.bv-modal-confirm>
|
||||
Delete
|
||||
</b-button>
|
||||
<b-modal id="bv-modal-confirm" @ok="deleteWorkspace" ok-title="Delete" ok-variant="danger" title-class="confirm-header" hide-backdrop title="Confirm workspace delete">
|
||||
<p>Are you sure you want to delete <strong>{{ workspace.name }}</strong> workspace from Lens?</p>
|
||||
</b-modal>
|
||||
</b-col>
|
||||
</b-form-row>
|
||||
</b-form>
|
||||
</div>
|
||||
<div class="col-3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClosePageButton from "@/_vue/components/common/ClosePageButton";
|
||||
|
||||
export default {
|
||||
name: 'EditWorkspacePage',
|
||||
components: {
|
||||
ClosePageButton
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
errors: {
|
||||
name: null,
|
||||
description: null
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
workspace: function() {
|
||||
return this.$store.getters.workspaceById(this.$route.params.id)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateWorkspace: function() {
|
||||
this.$store.commit("updateWorkspace", this.workspace)
|
||||
this.$router.push({
|
||||
name: "workspaces-page"
|
||||
})
|
||||
},
|
||||
deleteWorkspace: async function() {
|
||||
await this.$store.commit("removeWorkspace", this.workspace)
|
||||
this.$router.push({
|
||||
name: "workspaces-page"
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted: function() {
|
||||
this.$store.commit("hideMenu");
|
||||
},
|
||||
destroyed: function() {
|
||||
this.$store.commit("showMenu");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#app > .main-view > .content {
|
||||
left: 70px;
|
||||
right: 70px;
|
||||
}
|
||||
h2 {
|
||||
i {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
}
|
||||
}
|
||||
.lens-workspaces {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
& input {
|
||||
background-color: #252729 !important;
|
||||
border: 0px !important;
|
||||
color: #87909c !important;
|
||||
}
|
||||
.header {
|
||||
padding-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<div class="landing-page">
|
||||
<b-container fluid class="h-100">
|
||||
<!-- wrapper for center/center align -->
|
||||
<b-row class="h-100">
|
||||
<b-col sm="12" class="my-auto">
|
||||
<div>
|
||||
<b-row align-h="center" class="banner">
|
||||
<b-col sm="8">
|
||||
<h1 v-if="clusters.length === 0" class="display-4 text-center">
|
||||
Welcome!
|
||||
</h1>
|
||||
<p v-if="clusters.length === 0" class="text-center">
|
||||
Get started by associating one or more clusters to Lens.
|
||||
</p>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LandingPage',
|
||||
components: {},
|
||||
methods: {},
|
||||
computed: {
|
||||
workspaces: function() {
|
||||
return this.$store.getters.workspaces;
|
||||
},
|
||||
clusters: function() {
|
||||
return this.$store.getters.clusters;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.landing-page{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #282b2f url(../assets/img/crane.svg) no-repeat;
|
||||
background-position: 0px 35%;
|
||||
background-size: 85%;
|
||||
background-clip: content-box;
|
||||
.banner {
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
font-weight: normal;
|
||||
}
|
||||
p {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,72 +0,0 @@
|
||||
<template>
|
||||
<span>
|
||||
<a
|
||||
id="add-cluster"
|
||||
class="add-cluster menu-item"
|
||||
:class="[{
|
||||
'active':isActive(),
|
||||
}]"
|
||||
@click.prevent="addCluster"
|
||||
v-b-tooltip.hover.right
|
||||
title="Add clusters"
|
||||
>
|
||||
<i class="material-icons">add_box</i>
|
||||
<span v-if="newContexts.length > 0" class="badge badge-success">{{ newContexts.length }}</span>
|
||||
</a>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClustersMixin from "@/_vue/mixins/ClustersMixin";
|
||||
import { newContexts } from '@kubernetes/client-node/dist/config_types';
|
||||
export default {
|
||||
name: "AddClusterMenuItem",
|
||||
mixins: [ClustersMixin],
|
||||
data(){
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
methods: {
|
||||
isActive: function( id ){
|
||||
return "new" === this.$route.params.id;
|
||||
},
|
||||
addCluster: async function(){
|
||||
this.$router.push({
|
||||
name: "add-cluster-page",
|
||||
params: {
|
||||
id: "new"
|
||||
},
|
||||
}).catch(err => {})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log(this.newContexts.length)
|
||||
this.$store.dispatch("reloadAvailableKubeContexts");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.add-cluster{
|
||||
position: relative;
|
||||
display: block;
|
||||
color: #fff;
|
||||
padding: 6px 8px 6px 9px;
|
||||
opacity: 0.4;
|
||||
cursor: pointer;
|
||||
&.active{
|
||||
opacity: 0.75;
|
||||
}
|
||||
i {
|
||||
font-size: 50px;
|
||||
}
|
||||
.badge {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,166 +0,0 @@
|
||||
<template>
|
||||
<a
|
||||
class="menu-item"
|
||||
:class="[{
|
||||
'active':isActive,
|
||||
'online':(activated && !isActive),
|
||||
}]"
|
||||
@click.prevent="openClusterPage()"
|
||||
@contextmenu.prevent="openContextMenu()"
|
||||
:title="preferences.clusterName"
|
||||
v-b-tooltip.hover.right
|
||||
>
|
||||
<div
|
||||
class="icon-frame"
|
||||
:class="[{
|
||||
'active':isActive
|
||||
}]"
|
||||
>
|
||||
<img v-if="preferences.icon" :src="preferences.icon">
|
||||
<hashicon v-else :name="preferences.clusterName" size="38" />
|
||||
<span v-if="isAdmin && eventCount > 0" class="badge badge-danger">{{ eventCount >= 1000 ? "1000+" : eventCount }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import hashicon from "../hashicon/hashicon";
|
||||
import { setInterval, clearInterval } from "timers";
|
||||
const { remote } = require('electron')
|
||||
const { Menu, MenuItem } = remote
|
||||
|
||||
export default {
|
||||
name: "ClusterMenuItem",
|
||||
components: {
|
||||
hashicon
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
eventPoller: null,
|
||||
eventCount: 0,
|
||||
activated: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
cluster: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: null
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
preferences: function() {
|
||||
return this.cluster.preferences;
|
||||
},
|
||||
clusterAccessible: function() {
|
||||
return this.$store.getters.clusterById(this.cluster.id).accessible;
|
||||
},
|
||||
isActive: function() {
|
||||
return this.cluster.id === this.$route.params.id;
|
||||
},
|
||||
isAdmin: function() {
|
||||
return this.cluster.isAdmin;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openClusterPage() {
|
||||
this.activated = true;
|
||||
this.$router.push({
|
||||
name: "cluster-page",
|
||||
params: {
|
||||
id: this.cluster.id
|
||||
},
|
||||
}).catch(err => {})
|
||||
},
|
||||
openContextMenu(){
|
||||
const self = this;
|
||||
const menu = new Menu()
|
||||
menu.append(new MenuItem({ label: 'Settings', click() {
|
||||
self.$router.push({
|
||||
name: "cluster-settings-page",
|
||||
params: {
|
||||
id: self.cluster.id
|
||||
},
|
||||
}).catch(err => {})
|
||||
} }))
|
||||
if (this.activated) {
|
||||
menu.append(new MenuItem({ label: 'Disconnect', click() {
|
||||
self.activated = false;
|
||||
self.stopPolling();
|
||||
self.$store.dispatch("stopCluster", self.cluster.id);
|
||||
if (self.isActive) {
|
||||
self.$router.push({
|
||||
name: "landing-page"
|
||||
}).catch(err => {})
|
||||
}
|
||||
} }))
|
||||
}
|
||||
|
||||
menu.popup({ window: remote.getCurrentWindow() })
|
||||
},
|
||||
toggleEventPolling: function() {
|
||||
if(this.clusterAccessible && !this.eventPoller && this.activated && !this.isActive) {
|
||||
this.fetchEvents()
|
||||
this.eventPoller = setInterval(async () => {
|
||||
await this.fetchEvents()
|
||||
}, 30 * 1000);
|
||||
} else {
|
||||
this.eventCount = 0;
|
||||
this.stopPolling()
|
||||
}
|
||||
},
|
||||
fetchEvents: async function() {
|
||||
try {
|
||||
this.eventCount = await this.$promiseIpc.send("getClusterEvents", this.cluster.id);
|
||||
} catch (error) {
|
||||
console.error("Failed to get event count for cluster:", error)
|
||||
}
|
||||
},
|
||||
stopPolling: function() {
|
||||
if (this.eventPoller) {
|
||||
clearInterval(this.eventPoller)
|
||||
this.eventPoller = null
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
clusterAccessible: "toggleEventPolling",
|
||||
isActive: "toggleEventPolling"
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.stopPolling();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.badge {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
padding: 4px;
|
||||
i {
|
||||
font-size: 8px;
|
||||
}
|
||||
}
|
||||
img {
|
||||
width: 38px;
|
||||
max-height: 42px;
|
||||
}
|
||||
span.hashicon {
|
||||
margin-top: 6px;
|
||||
}
|
||||
div.icon-frame {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.active, &:hover {
|
||||
margin-left: -3px;
|
||||
padding: 0px 0px 0px 3px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,108 +0,0 @@
|
||||
<template>
|
||||
<div :style="{ paddingTop: computedPaddingTop }" class="main-menu">
|
||||
<draggable v-model="clusters">
|
||||
<ClusterMenuItem
|
||||
v-for="cluster in clusters" :key="cluster.id"
|
||||
:cluster="cluster"
|
||||
/>
|
||||
</draggable>
|
||||
<AddClusterMenuItem />
|
||||
<b-tooltip v-if="clusters.length === 0" show target="add-cluster" placement="rightbottom">
|
||||
<p class="text-left">
|
||||
This is the quick launch menu.
|
||||
</p>
|
||||
<p class="text-left">
|
||||
Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button.
|
||||
</p>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClusterMenuItem from "@/_vue/components/MainMenu/ClusterMenuItem";
|
||||
import AddClusterMenuItem from "@/_vue/components/MainMenu/AddClusterMenuItem";
|
||||
import draggable from 'vuedraggable'
|
||||
import { clusterStore } from "../../../../common/cluster-store"
|
||||
import { isMac } from "../../../../common/vars"
|
||||
|
||||
const {remote} = require('electron')
|
||||
const {Menu, MenuItem} = remote
|
||||
|
||||
export default {
|
||||
name: "MainMenu",
|
||||
components: {
|
||||
ClusterMenuItem,
|
||||
AddClusterMenuItem,
|
||||
draggable
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
computedPaddingTop: "15px"
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
clusters: {
|
||||
get: function () {
|
||||
return this.$store.getters.clusters;
|
||||
},
|
||||
set: function (clusters) {
|
||||
this.$store.commit("updateClusters", clusters);
|
||||
clusterStore.storeClusters(clusters);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isActive: function (id) {
|
||||
return id === this.$route.params.id;
|
||||
},
|
||||
addCluster: async function () {
|
||||
this.$router.push({
|
||||
name: "add-cluster-page",
|
||||
params: {
|
||||
id: "new"
|
||||
},
|
||||
}).catch(err => {})
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
if (isMac) {
|
||||
this.computedPaddingTop = "25px";
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.main-menu {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 20px;
|
||||
width: 70px;
|
||||
padding-top: 15px;
|
||||
background: #252729;
|
||||
overflow: hidden;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
padding: 6px 15px 6px 15px;
|
||||
color: #87909c;
|
||||
opacity: 0.3;
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.online {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,256 +0,0 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<ClosePageButton />
|
||||
<div class="container-fluid lens-preferences">
|
||||
<div class="header sticky-top">
|
||||
<h2>Preferences</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3" />
|
||||
<div ref="content" class="global-settings col-6">
|
||||
<h2>Color Theme</h2>
|
||||
|
||||
<b-form-group
|
||||
label="Cluster Color theme:"
|
||||
label-for="input-color-theme"
|
||||
>
|
||||
<b-form-select
|
||||
id="input-color-theme"
|
||||
v-model="preferences.colorTheme"
|
||||
@change="onSave"
|
||||
:options="colorThemeOptions"
|
||||
/>
|
||||
</b-form-group>
|
||||
</div>
|
||||
<div class="col-3" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3" />
|
||||
<div ref="content" class="global-settings col-6">
|
||||
<h2>Download Mirror</h2>
|
||||
|
||||
<b-form-group
|
||||
label="Download mirror for kubectl:"
|
||||
label-for="input-download-mirror"
|
||||
>
|
||||
<b-form-select
|
||||
id="input-download-mirror"
|
||||
v-model="preferences.downloadMirror"
|
||||
@change="onSave"
|
||||
:options="downloadMirrorOptions"
|
||||
/>
|
||||
</b-form-group>
|
||||
</div>
|
||||
<div class="col-3" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3" />
|
||||
<div ref="content" class="global-settings col-6">
|
||||
<h2>Helm</h2>
|
||||
|
||||
<b-form-group
|
||||
label="Repositories:"
|
||||
label-for="textarea-helm-repositories"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-select
|
||||
:options="hubRepositories"
|
||||
v-model="selectedHubRepo"
|
||||
>
|
||||
<template v-slot:first>
|
||||
<option value="">
|
||||
Select Helm repository
|
||||
</option>
|
||||
</template>
|
||||
</b-form-select>
|
||||
<b-input-group-append>
|
||||
<b-button :disabled="!selectedHubRepo" @click="addHubRepo" variant="primary">
|
||||
Add <b-spinner small v-if="isHelmProcessing" label="Small Spinner" />
|
||||
</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
<b-list-group>
|
||||
<b-list-group-item v-for="repo in helmRepos" :key="repo.name">
|
||||
<span v-b-tooltip.hover v-b-tooltip.right :title="repo.url">{{ repo.name }}</span>
|
||||
<button @click="removeRepo(repo)" :data-repo="repo.name" class="close">
|
||||
<i class="material-icons">delete</i>
|
||||
</button>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</div>
|
||||
<div class="col-3" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3" />
|
||||
<div ref="content" class="global-settings col-6">
|
||||
<h2>HTTP Proxy</h2>
|
||||
|
||||
<b-form-group
|
||||
label="Proxy URL:"
|
||||
label-for="input-https-proxy"
|
||||
description="Proxy is used only for non-cluster communication."
|
||||
>
|
||||
<b-form-input
|
||||
id="input-https-proxy"
|
||||
v-model="preferences.httpsProxy"
|
||||
trim
|
||||
@blur="onSave"
|
||||
placeholder="Type HTTP proxy url (example: http://proxy.acme.org:8080)"
|
||||
/>
|
||||
</b-form-group>
|
||||
<h2>Certificate Trust</h2>
|
||||
<b-form-group
|
||||
label-for="checkbox-allow-untrusted-cas"
|
||||
description="This will make Lens to trust ANY certificate authority without any validations. Needed with some corporate proxies that do certificate re-writing. Does not affect cluster communications!"
|
||||
>
|
||||
<b-form-checkbox
|
||||
id="checkbox-allow-untrusted-cas"
|
||||
switch
|
||||
v-model="preferences.allowUntrustedCAs"
|
||||
@input="onSave"
|
||||
>
|
||||
Allow untrusted Certificate Authorities
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
<h2>Telemetry & Usage Tracking</h2>
|
||||
<b-form-group
|
||||
label-for="checkbox-allow-telemetry"
|
||||
description="Telemetry & usage data is collected to continuously improve the Lens experience."
|
||||
>
|
||||
<b-form-checkbox
|
||||
id="checkbox-allow-telemetry"
|
||||
switch
|
||||
v-model="preferences.allowTelemetry"
|
||||
@input="onSave"
|
||||
>
|
||||
Allow telemetry & usage tracking
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</div>
|
||||
<div class="col-3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from 'request-promise-native'
|
||||
import { globalRequestOpts } from "../../../common/request"
|
||||
import ClosePageButton from "@/_vue/components/common/ClosePageButton";
|
||||
|
||||
export default {
|
||||
name: 'PreferencesPage',
|
||||
components: {
|
||||
ClosePageButton
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
hubRepositories: [],
|
||||
selectedHubRepo: "",
|
||||
colorThemeOptions: [
|
||||
{ value: "dark", text: "Dark" },
|
||||
{ value: "light", text: "Light" },
|
||||
],
|
||||
downloadMirrorOptions: [
|
||||
{ value: "default", text: "Default (Google)" },
|
||||
{ value: "china", text: "China (Azure)" },
|
||||
],
|
||||
isHelmProcessing: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
helmRepos: function() {
|
||||
return this.$store.getters.repos;
|
||||
},
|
||||
preferences: function() {
|
||||
return this.$store.getters.preferences;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onSave: function() {
|
||||
this.$store.commit("savePreferences", this.preferences);
|
||||
},
|
||||
request: async function(opts) {
|
||||
const o = Object.assign({
|
||||
timeout: 10000,
|
||||
headers: {},
|
||||
resolveWithFullResponse: true,
|
||||
json: true
|
||||
}, globalRequestOpts(opts))
|
||||
|
||||
return await request(o)
|
||||
},
|
||||
repoCompare: function( a, b ) {
|
||||
if ( a.name < b.name ){
|
||||
return -1;
|
||||
}
|
||||
if ( a.name > b.name ){
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
addHubRepo: async function() {
|
||||
if(!this.selectedHubRepo || this.selectedHubRepo === "") {
|
||||
return
|
||||
}
|
||||
this.isHelmProcessing = true
|
||||
const [name, url ] = this.selectedHubRepo.split("|")
|
||||
await this.$store.dispatch('addHelmRepo', { name, url })
|
||||
this.selectedHubRepo = ""
|
||||
this.isHelmProcessing = false
|
||||
},
|
||||
removeRepo: async function(repo) {
|
||||
await this.$store.dispatch('removeHelmRepo', repo)
|
||||
},
|
||||
loadHubRepositories: async function() {
|
||||
const res = await this.request({ uri: "https://hub.helm.sh/assets/js/repos.json"}).catch((error) => { this.hubRepositories = [] })
|
||||
this.hubRepositories = res.body.data.sort(this.repoCompare).map((repo) => {
|
||||
return { text: repo.name, value: repo.name+"|"+repo.url}
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$store.dispatch('refreshHelmRepos')
|
||||
},
|
||||
mounted: async function() {
|
||||
this.$store.commit("hideMenu");
|
||||
this.loadHubRepositories()
|
||||
},
|
||||
destroyed: function() {
|
||||
this.$store.commit("showMenu");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#app > .main-view > .content {
|
||||
left: 70px;
|
||||
}
|
||||
.lens-preferences {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
& input {
|
||||
background-color: #252729 !important;
|
||||
border: 0px !important;
|
||||
color: #87909c !important;
|
||||
}
|
||||
.header {
|
||||
padding-top: 15px;
|
||||
}
|
||||
.close {
|
||||
text-shadow: none;
|
||||
}
|
||||
.list-group-item {
|
||||
background-color: #252729 !important;
|
||||
color: #87909c !important;
|
||||
}
|
||||
& i {
|
||||
color: #87909c !important;
|
||||
}
|
||||
}
|
||||
|
||||
.global-settings {
|
||||
padding-top: 0.75em;
|
||||
}
|
||||
</style>
|
||||
@@ -1,113 +0,0 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<div class="container-fluid whats-new">
|
||||
<div class="row">
|
||||
<div class="col-md whats-new-text-wrapper">
|
||||
<div class="logo">
|
||||
<img src="../assets/img/lens-logo.svg">
|
||||
</div>
|
||||
<!-- Safe to use v-html as we self generate the html locally only -->
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div class="whats-new-text" v-html="content">
|
||||
<!-- What's new content -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md whats-new-actions-wrapper">
|
||||
<div class="whats-new-actions">
|
||||
<button type="button" class="btn btn-primary" @click="toLanding">
|
||||
Ok, got it!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import marked from 'marked'
|
||||
import {readFileSync} from 'fs'
|
||||
import { getStaticPath } from "../../../common/register-static"
|
||||
|
||||
export default {
|
||||
name: 'WhatsNewPage',
|
||||
data() {
|
||||
let releaseNotes = getStaticPath("RELEASE_NOTES.md");
|
||||
let content = marked(readFileSync(releaseNotes, 'utf8'));
|
||||
return {
|
||||
error: "",
|
||||
content: content,
|
||||
}
|
||||
},
|
||||
components: {},
|
||||
computed: {
|
||||
},
|
||||
methods: {
|
||||
toLanding: async function() {
|
||||
await this.$store.dispatch("updateLastSeenAppVersion")
|
||||
this.$router.push({
|
||||
name: "landing-page",
|
||||
}).catch(err => {})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.content {
|
||||
background: #282b2f url(../assets/img/crane.svg) no-repeat;
|
||||
background-position: 0px 35%;
|
||||
background-size: 85%;
|
||||
background-clip: content-box;
|
||||
}
|
||||
.content > .whats-new{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color:rgba(0, 0, 0, 0.3);
|
||||
|
||||
.whats-new-text-wrapper{
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
bottom: 77px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
overflow-y: scroll;
|
||||
.logo {
|
||||
padding: 30px 30px;
|
||||
max-width: 1000px;
|
||||
img {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
.whats-new-text{
|
||||
padding: 10px 30px;
|
||||
max-width: 1000px;
|
||||
h1{
|
||||
font-size: 24px;
|
||||
}
|
||||
h2{
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.whats-new-actions-wrapper{
|
||||
position: absolute;
|
||||
height: 77px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-top: 1px solid #333;
|
||||
background: var(--lens-pane-bg);
|
||||
.whats-new-actions{
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,156 +0,0 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<ClosePageButton />
|
||||
<div class="container-fluid lens-workspaces">
|
||||
<div class="header sticky-top">
|
||||
<h2><i class="material-icons">layers</i> Workspaces</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-9 main-content">
|
||||
<b-list-group>
|
||||
<b-list-group-item @click="editWorkspace(workspace)" href="#" v-for="workspace in workspaces" :key="workspace.id" :workspace="workspace">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5>{{ workspace.name }}</h5>
|
||||
<small v-if="workspace.id !== 'default'">
|
||||
<a href="#" title="Switch to workspace" v-b-tooltip.hover.right @click.prevent.stop="switchWorkspace(workspace)"><i class="material-icons">keyboard_arrow_right</i></a>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<p class="mb-1">
|
||||
{{ workspace.description }}
|
||||
</p>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
|
||||
<b-button variant="primary" href="#" @click.prevent="goAdd">
|
||||
Add Workspace
|
||||
</b-button>
|
||||
</div>
|
||||
<div class="col-3 help">
|
||||
<h3>What is a Workspace?</h3>
|
||||
<p>
|
||||
Workspaces are used to organize number of clusters into logical groups. A single workspaces contains a list of clusters and their full configuration.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClosePageButton from "@/_vue/components/common/ClosePageButton";
|
||||
export default {
|
||||
name: 'WorkspacesPage',
|
||||
components: {
|
||||
ClosePageButton
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
fields: [
|
||||
{
|
||||
key: "name",
|
||||
label: "Name"
|
||||
},
|
||||
{
|
||||
key: "description",
|
||||
label: "Description"
|
||||
}
|
||||
],
|
||||
errors: {
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
workspaces: function() {
|
||||
return this.$store.getters.workspaces;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
switchWorkspace: function(workspace) {
|
||||
this.$store.commit("setCurrentWorkspace", workspace);
|
||||
this.$store.dispatch("clearClusters");
|
||||
this.$store.dispatch("refreshClusters", workspace);
|
||||
this.$router.push({
|
||||
name: "landing-page"
|
||||
});
|
||||
},
|
||||
editWorkspace: function(workspace) {
|
||||
this.$router.push({
|
||||
name: "edit-workspace-page",
|
||||
params: {
|
||||
id: workspace.id
|
||||
}
|
||||
});
|
||||
},
|
||||
goAdd: function() {
|
||||
this.$router.push({
|
||||
name: "add-workspace-page"
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted: function() {
|
||||
this.$store.commit("hideMenu");
|
||||
},
|
||||
destroyed: function() {
|
||||
this.$store.commit("showMenu");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#app > .main-view > .content {
|
||||
left: 70px;
|
||||
right: 70px;
|
||||
}
|
||||
h2 {
|
||||
i {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
}
|
||||
}
|
||||
.main-content {
|
||||
padding-left: 40px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
.lens-workspaces {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
& input {
|
||||
background-color: #252729 !important;
|
||||
border: 0px !important;
|
||||
color: #87909c !important;
|
||||
}
|
||||
.header {
|
||||
padding-top: 15px;
|
||||
}
|
||||
.list-group {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.list-group-item {
|
||||
background-color: inherit;
|
||||
color: var(--lens-text-color-light);
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--lens-pane-bg);
|
||||
padding-left: 0;
|
||||
h5 {
|
||||
color: var(--lens-primary)
|
||||
}
|
||||
h5:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
small {
|
||||
a {
|
||||
color: var(--lens-text-color-light);
|
||||
z-index: 100;
|
||||
}
|
||||
i {
|
||||
opacity: 0.3;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,40 +0,0 @@
|
||||
<template>
|
||||
<div class="close-page-button">
|
||||
<a @click.prevent="navigateBack" href="#">
|
||||
<i class="material-icons">close</i>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "ClosePageButton",
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
navigateBack: () => {
|
||||
window.history.back();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
div.close-page-button {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 25px;
|
||||
z-index: 1100;
|
||||
a {
|
||||
position: relative;
|
||||
display: block;
|
||||
color: #fff;
|
||||
opacity: 0.4;
|
||||
}
|
||||
i {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<span class="hashicon" ref="hashicon" />
|
||||
</template>
|
||||
<script>
|
||||
import hashicon from 'hashicon'
|
||||
|
||||
export default {
|
||||
name:'Hashicon',
|
||||
props:{
|
||||
name: {
|
||||
default: null,
|
||||
type: String
|
||||
},
|
||||
size: {
|
||||
default: null,
|
||||
type: String
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const params = {
|
||||
size: this.size || 48,
|
||||
lightness: { min: 45, max: 65 },
|
||||
saturation: { min: 30, max: 60 }
|
||||
}
|
||||
this.$refs.hashicon.appendChild(hashicon(this.name, params))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,47 +0,0 @@
|
||||
import "../../common/system-ca"
|
||||
import "./assets/css/app.scss"
|
||||
import "prismjs";
|
||||
import "prismjs/components/prism-yaml"
|
||||
import { remote } from "electron"
|
||||
import Vue from 'vue'
|
||||
import VueElectron from 'vue-electron'
|
||||
import BootstrapVue from 'bootstrap-vue'
|
||||
import { PromiseIpc } from 'electron-promise-ipc'
|
||||
import { Tracker } from "../../common/tracker"
|
||||
import App from './App'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
|
||||
const tracker = new Tracker(remote.app);
|
||||
const promiseIpc = new PromiseIpc({maxTimeoutMs: 6000});
|
||||
|
||||
promiseIpc.on('navigate', async (view) => {
|
||||
router.push(view).catch(err => {})
|
||||
});
|
||||
|
||||
Vue.config.productionTip = false
|
||||
Vue.use(VueElectron)
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
Vue.mixin({
|
||||
created: function () {
|
||||
this.$promiseIpc = promiseIpc;
|
||||
this.$tracker = tracker;
|
||||
}
|
||||
})
|
||||
|
||||
// any initialization we want to do for app state
|
||||
setTimeout(() => {
|
||||
store.dispatch('init', ).catch((error) => {
|
||||
console.error(error)
|
||||
}).finally(() => {
|
||||
/* eslint-disable no-new */
|
||||
console.log("start vue")
|
||||
new Vue({
|
||||
components: { App },
|
||||
store,
|
||||
router,
|
||||
template: '<App/>'
|
||||
}).$mount('#app')
|
||||
})
|
||||
}, 0)
|
||||
@@ -1,16 +0,0 @@
|
||||
export default {
|
||||
computed: {
|
||||
clusters: function() {
|
||||
return this.$store.getters.clusters
|
||||
},
|
||||
newContexts: function() {
|
||||
const seenContexts = this.seenContexts || this.$store.getters.seenContexts
|
||||
const contextNamesFromKubeconfig = this.availableContexts.map(item => item.currentContext)
|
||||
return contextNamesFromKubeconfig.filter((item) => seenContexts.indexOf(item) < 0)
|
||||
},
|
||||
availableContexts: function() {
|
||||
// read available kubeconfigs from store on filter out configs already found in added clusters
|
||||
return this.$store.getters.availableKubeContexts.filter(item => !this.clusters.find((cluster) => cluster.contextName == item.currentContext));
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import store from "../store";
|
||||
import { whatsNew } from './routeguard'
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
const router = new Router({
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'landing-page',
|
||||
component: require('@/_vue/components/LandingPage').default,
|
||||
meta: {
|
||||
routeguard: [
|
||||
// guards in priority order; the first one to catch will trigger something
|
||||
whatsNew,
|
||||
],
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/preferences',
|
||||
name: 'preferences-page',
|
||||
component: require('@/_vue/components/PreferencesPage').default,
|
||||
},
|
||||
{
|
||||
path: '/workspaces',
|
||||
name: 'workspaces-page',
|
||||
component: require('@/_vue/components/WorkspacesPage').default,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/add-workspace',
|
||||
name: 'add-workspace-page',
|
||||
component: require('@/_vue/components/AddWorkspacePage').default,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/edit-workspace',
|
||||
name: 'edit-workspace-page',
|
||||
component: require('@/_vue/components/EditWorkspacePage').default,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/clusters/:id',
|
||||
name: 'cluster-page',
|
||||
component: require('@/_vue/components/ClusterPage').default,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/clusters/:id/settings',
|
||||
name: 'cluster-settings-page',
|
||||
component: require('@/_vue/components/ClusterSettings').default,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/add-cluster",
|
||||
name: "add-cluster-page",
|
||||
component: require('@/_vue/components/AddClusterPage').default,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/whats-new",
|
||||
name: "whats-new-page",
|
||||
component: require('@/_vue/components/WhatsNewPage').default,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
redirect: '/'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
|
||||
// guard routes
|
||||
if(to.meta && to.meta.routeguard && to.meta.routeguard.length > 0){
|
||||
|
||||
let guardNext;
|
||||
to.meta.routeguard.forEach(guard => {
|
||||
if(!guardNext) guardNext = guard(to, from, store);
|
||||
});
|
||||
|
||||
if(guardNext) {
|
||||
next(guardNext);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
next();
|
||||
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,26 +0,0 @@
|
||||
export function auth ( to, from, store ) {
|
||||
if(!store.getters.isLoggedIn){
|
||||
console.log("router: guard: auth: activated");
|
||||
return {
|
||||
path: '/login'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function eula ( to, from, store ) {
|
||||
if(!store.getters.isEulaAccepted){
|
||||
console.log("router: guard: eula: activated");
|
||||
return {
|
||||
path: '/accept-eula'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function whatsNew( to, from, store ) {
|
||||
if(store.getters.showWhatsNew){
|
||||
console.log("router: guard: whatsNew: activated");
|
||||
return {
|
||||
path: '/whats-new'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import semver from "semver"
|
||||
import { userStore } from "../../../common/user-store"
|
||||
import { getAppVersion } from "../../../common/utils/app-version"
|
||||
import KubeContexts from './modules/kube-contexts'
|
||||
import Clusters from './modules/clusters'
|
||||
import HelmRepos from './modules/helm-repos'
|
||||
import Workspaces from './modules/workspaces'
|
||||
|
||||
// promise ipc
|
||||
import { PromiseIpc } from 'electron-promise-ipc'
|
||||
const promiseIpc = new PromiseIpc( { maxTimeoutMs: 120000 } );
|
||||
|
||||
// tracker
|
||||
import { Tracker } from "../../../common/tracker"
|
||||
import { remote } from "electron"
|
||||
const tracker = new Tracker(remote.app);
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
Clusters,
|
||||
HelmRepos,
|
||||
KubeContexts,
|
||||
Workspaces
|
||||
},
|
||||
state: {
|
||||
preferences: {},
|
||||
hud: {
|
||||
isMenuVisible: true,
|
||||
},
|
||||
seenContexts: userStore.getSeenContexts(),
|
||||
lastSeenAppVersion: userStore.lastSeenAppVersion(),
|
||||
},
|
||||
mutations: {
|
||||
storeSeenContexts(state, context) {
|
||||
const seenContexts = userStore.storeSeenContext(context);
|
||||
state.seenContexts = seenContexts
|
||||
},
|
||||
updateLastSeenAppVersion(state, appVersion) {
|
||||
state.lastSeenAppVersion = appVersion;
|
||||
userStore.setLastSeenAppVersion(appVersion)
|
||||
},
|
||||
loadPreferences(state) {
|
||||
this.commit("savePreferences", userStore.getPreferences());
|
||||
},
|
||||
savePreferences(state, prefs) {
|
||||
if (prefs.allowTelemetry) {
|
||||
tracker.event("telemetry", "enabled")
|
||||
} else {
|
||||
tracker.event("telemetry", "disabled")
|
||||
}
|
||||
state.preferences = prefs;
|
||||
userStore.setPreferences(prefs);
|
||||
this.dispatch("destroyWebviews")
|
||||
promiseIpc.send("preferencesSaved")
|
||||
},
|
||||
hideMenu(state) {
|
||||
state.hud.isMenuVisible = false;
|
||||
},
|
||||
showMenu(state) {
|
||||
state.hud.isMenuVisible = true;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async init({commit, getters}) {
|
||||
commit("loadPreferences");
|
||||
|
||||
await this.dispatch('refreshClusters', getters.currentWorkspace);
|
||||
|
||||
return true;
|
||||
},
|
||||
async addSeenContexts({commit}, data){
|
||||
commit('storeSeenContexts', data);
|
||||
},
|
||||
async updateLastSeenAppVersion({commit, state}) {
|
||||
tracker.event("app", "whats-new-seen")
|
||||
commit("updateLastSeenAppVersion", getAppVersion())
|
||||
}
|
||||
},
|
||||
getters : {
|
||||
seenContexts: state => state.seenContexts,
|
||||
hud: state => state.hud,
|
||||
isMenuVisible: function(state, getters){
|
||||
return state.hud.isMenuVisible && !getters.showWhatsNew;
|
||||
},
|
||||
showWhatsNew: function(state) {
|
||||
return semver.gt(getAppVersion(), state.lastSeenAppVersion);
|
||||
},
|
||||
preferences: state => state.preferences,
|
||||
}
|
||||
});
|
||||
@@ -1,279 +0,0 @@
|
||||
import Vue from "vue"
|
||||
import { ClusterInfo } from "../../../../main/cluster"
|
||||
import { MutationTree, ActionTree, GetterTree } from "vuex"
|
||||
import { PromiseIpc } from 'electron-promise-ipc'
|
||||
import { Tracker } from "../../../../common/tracker"
|
||||
import { remote } from "electron"
|
||||
import { clusterStore } from "../../../../common/cluster-store"
|
||||
import { Workspace } from "../../../../common/workspace-store"
|
||||
|
||||
const promiseIpc = new PromiseIpc( { maxTimeoutMs: 120000 } );
|
||||
const tracker = new Tracker(remote.app);
|
||||
|
||||
export interface LensWebview {
|
||||
id: string;
|
||||
loaded: boolean;
|
||||
webview?: HTMLIFrameElement;
|
||||
}
|
||||
|
||||
export interface ClusterState {
|
||||
lenses: LensWebview[];
|
||||
clusters: ClusterInfo[];
|
||||
}
|
||||
|
||||
const state: ClusterState = {
|
||||
lenses: [],
|
||||
clusters: []
|
||||
}
|
||||
|
||||
const actions: ActionTree<ClusterState, any> = {
|
||||
async refreshClusters({commit}, currentWorkspace: Workspace) {
|
||||
const clusters: ClusterInfo[] = await promiseIpc.send('getClusters', currentWorkspace.id).catch((error: Error) => {
|
||||
return false;
|
||||
})
|
||||
if(!clusters) return false;
|
||||
commit('updateClusters', clusters);
|
||||
clusters.forEach((cluster: ClusterInfo) => {
|
||||
const lens: LensWebview = {
|
||||
id: cluster.id,
|
||||
webview: null,
|
||||
loaded: false
|
||||
};
|
||||
commit("updateLens", lens)
|
||||
})
|
||||
return true;
|
||||
},
|
||||
async getCluster({commit, getters}, id: string) {
|
||||
const cluster: ClusterInfo = getters.clusters.find((c: ClusterInfo) => c.id === id)
|
||||
if(!cluster) return null;
|
||||
|
||||
const remoteCluster = await promiseIpc.send("getCluster", cluster.id)
|
||||
if(!remoteCluster) return null;
|
||||
|
||||
Object.assign(cluster, remoteCluster)
|
||||
commit('updateCluster', cluster);
|
||||
|
||||
return cluster;
|
||||
},
|
||||
async refineCluster({commit}, id: string) {
|
||||
console.log("VUEX: ACTION: REFINE CLUSTER", id);
|
||||
|
||||
const remoteCluster = await promiseIpc.send("getCluster", id)
|
||||
if(!remoteCluster) return null;
|
||||
|
||||
commit('updateCluster', remoteCluster);
|
||||
|
||||
return remoteCluster;
|
||||
},
|
||||
async stopCluster({dispatch, getters}, id: string) {
|
||||
const cluster: ClusterInfo = getters.clusters.find((c: ClusterInfo) => c.id === id)
|
||||
if (!cluster) return;
|
||||
|
||||
const lens = getters.lensById(cluster.id)
|
||||
if (lens) {
|
||||
await dispatch("detachWebview", lens)
|
||||
await promiseIpc.send("stopCluster", cluster.id)
|
||||
tracker.event("cluster", "stop")
|
||||
}
|
||||
},
|
||||
async removeCluster({getters, dispatch}, id: string) {
|
||||
const cluster: ClusterInfo = getters.clusters.find((c: ClusterInfo) => c.id === id)
|
||||
if (!cluster) {
|
||||
return
|
||||
}
|
||||
const lens = this.getters.lensById(cluster.id)
|
||||
if (lens) {
|
||||
dispatch("detachWebview", lens)
|
||||
}
|
||||
await promiseIpc.send("removeCluster", cluster.id).catch((error: Error) => {
|
||||
return false;
|
||||
})
|
||||
tracker.event("cluster", "remove");
|
||||
await dispatch("refreshClusters", getters.currentWorkspace)
|
||||
return true;
|
||||
},
|
||||
async addCluster({commit, getters, dispatch}, data) {
|
||||
const res = await promiseIpc.send("addCluster", data)
|
||||
if(!res) return null;
|
||||
|
||||
tracker.event("cluster", "add");
|
||||
commit('updateClusters', res.allClusters);
|
||||
await dispatch("refreshClusters", getters.currentWorkspace);
|
||||
return res.addedCluster;
|
||||
},
|
||||
async clearClusters({commit, getters, dispatch}){
|
||||
// todo: clean from main process as well?
|
||||
getters.lenses.forEach((lens: LensWebview) => {
|
||||
if (lens.webview) {
|
||||
dispatch("detachWebview", lens)
|
||||
}
|
||||
})
|
||||
commit('updateLenses', []);
|
||||
commit('updateClusters', []);
|
||||
return true;
|
||||
},
|
||||
|
||||
async uploadClusterIcon({commit}, data) {
|
||||
const res = await promiseIpc.send("saveClusterIcon", data)
|
||||
tracker.event("cluster", "upload-icon")
|
||||
if (res.cluster) commit("updateCluster", res.cluster)
|
||||
return res
|
||||
},
|
||||
|
||||
async resetClusterIcon({commit}, data) {
|
||||
const res = await promiseIpc.send("resetClusterIcon", data.clusterId)
|
||||
tracker.event("cluster", "reset-icon")
|
||||
if (res.cluster) commit("updateCluster", res.cluster)
|
||||
return res
|
||||
},
|
||||
|
||||
// For data structure see: cluster-manager.ts / FeatureInstallRequest
|
||||
async installClusterFeature({commit}, data) {
|
||||
// Custom no timeout IPC as install can take very variable time
|
||||
const ipc = new PromiseIpc();
|
||||
const response = await ipc.send('installFeature', data)
|
||||
console.log("installer result:", response);
|
||||
const cluster = await ipc.send('refreshCluster', data.clusterId)
|
||||
|
||||
tracker.event("cluster", "install-feature")
|
||||
commit("updateCluster", cluster)
|
||||
return response
|
||||
},
|
||||
// For data structure see: cluster-manager.ts / FeatureInstallRequest
|
||||
async upgradeClusterFeature({commit}, data) {
|
||||
// Custom no timeout IPC as install can take very variable time
|
||||
const ipc = new PromiseIpc();
|
||||
const response = await ipc.send('upgradeFeature', data)
|
||||
console.log("upgrade result:", response);
|
||||
const cluster = await ipc.send('refreshCluster', data.clusterId)
|
||||
|
||||
|
||||
tracker.event("cluster", "upgrade-feature")
|
||||
commit("updateCluster", cluster)
|
||||
return response
|
||||
},
|
||||
// For data structure see: cluster-manager.ts / FeatureInstallRequest
|
||||
async uninstallClusterFeature({commit}, data) {
|
||||
// Custom no timeout IPC as uninstall can take very variable time
|
||||
const ipc = new PromiseIpc();
|
||||
const response = await ipc.send('uninstallFeature', data)
|
||||
console.log("uninstaller result:", response);
|
||||
const cluster = await ipc.send('refreshCluster', data.clusterId)
|
||||
|
||||
tracker.event("cluster", "uninstall-feature")
|
||||
commit("updateCluster", cluster)
|
||||
return response
|
||||
},
|
||||
|
||||
attachWebview({commit}, lens: LensWebview) {
|
||||
const container: any = document.getElementById("lens-container");
|
||||
if (!container || !lens.webview) {
|
||||
return
|
||||
}
|
||||
container.style = "display: block;"
|
||||
let webview = null
|
||||
container.childNodes.forEach((child: any) => {
|
||||
if (child === lens.webview) {
|
||||
webview = child
|
||||
}
|
||||
})
|
||||
if (!webview) {
|
||||
container.appendChild(lens.webview)
|
||||
}
|
||||
container.childNodes.forEach((child: any) => {
|
||||
if (child !== lens.webview) {
|
||||
child.style = "display: none;"
|
||||
} else {
|
||||
child.style = "top: 0; bottom: 20px; position: absolute; width: 100%;"
|
||||
}
|
||||
})
|
||||
promiseIpc.send("enableClusterSettingsMenuItem", lens.id)
|
||||
},
|
||||
detachWebview({commit}, lens: LensWebview) {
|
||||
const container: any = document.getElementById("lens-container");
|
||||
if (!container) { return }
|
||||
container.childNodes.forEach((child: any) => {
|
||||
if (child === lens.webview) {
|
||||
container.removeChild(lens.webview)
|
||||
lens.webview = null
|
||||
lens.loaded = false
|
||||
commit("updateLens", lens)
|
||||
}
|
||||
})
|
||||
promiseIpc.send("disableClusterSettingsMenuItem")
|
||||
},
|
||||
hideWebviews({commit}) {
|
||||
const container: any = document.getElementById("lens-container");
|
||||
if (!container) { return }
|
||||
container.style = "display: none;"
|
||||
container.childNodes.forEach((child: any) => {
|
||||
child.style = "display: none;"
|
||||
})
|
||||
promiseIpc.send("disableClusterSettingsMenuItem")
|
||||
},
|
||||
destroyWebviews({commit}) {
|
||||
state.lenses.forEach((lens) => {
|
||||
this.dispatch("detachWebview", lens)
|
||||
})
|
||||
},
|
||||
storeCluster({commit}, cluster: ClusterInfo) {
|
||||
clusterStore.storeCluster(cluster);
|
||||
commit("updateCluster", cluster)
|
||||
promiseIpc.send("clusterStored", cluster.id)
|
||||
}
|
||||
}
|
||||
|
||||
const getters: GetterTree<ClusterState, any> = {
|
||||
clusters: state => state.clusters,
|
||||
clusterById: state => (id: string) => {
|
||||
const cluster = state.clusters.find(c => c.id === id);
|
||||
if (cluster) {
|
||||
return cluster;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
lenses: state => state.lenses,
|
||||
lensById: state => (id: string) => {
|
||||
const lens = state.lenses.find(c => c.id === id);
|
||||
if (lens) {
|
||||
return lens;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const mutations: MutationTree<ClusterState> = {
|
||||
updateClusters(state, clusters: ClusterInfo[]) {
|
||||
Vue.set(state, 'clusters', [...clusters])
|
||||
},
|
||||
updateCluster(state, cluster) {
|
||||
state.clusters.forEach((c, index) => {
|
||||
if(c.id === cluster.id) {
|
||||
Vue.set(state.clusters, index, cluster)
|
||||
}
|
||||
})
|
||||
},
|
||||
updateLenses(state, data) {
|
||||
Vue.set(state, 'lenses', [...data])
|
||||
},
|
||||
updateLens(state, lens: LensWebview) {
|
||||
const lensIndex = state.lenses.findIndex(l => l.id == lens.id);
|
||||
if (lensIndex >= 0) {
|
||||
state.lenses[lensIndex] = lens
|
||||
Vue.set(state.lenses, lensIndex, lens)
|
||||
} else {
|
||||
console.log("update new lens")
|
||||
state.lenses.push(lens)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: false,
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import Vue from "vue"
|
||||
import { MutationTree, ActionTree, GetterTree } from "vuex"
|
||||
import { HelmRepo, repoManager } from "../../../../main/helm-repo-manager"
|
||||
|
||||
export interface HelmRepoState {
|
||||
repos: HelmRepo[];
|
||||
}
|
||||
|
||||
const state: HelmRepoState = {
|
||||
repos: []
|
||||
}
|
||||
|
||||
const actions: ActionTree<HelmRepoState, any> = {
|
||||
async addHelmRepo({ commit }, data){
|
||||
const res = await repoManager.addRepo(data).catch((error: Error) => {
|
||||
return false;
|
||||
})
|
||||
if(!res) return false;
|
||||
return await this.dispatch("refreshHelmRepos")
|
||||
},
|
||||
async removeHelmRepo({ commit }, data){
|
||||
const res = await repoManager.removeRepo(data).catch((error: Error) => {
|
||||
return false;
|
||||
})
|
||||
if(!res) return false;
|
||||
return await this.dispatch("refreshHelmRepos")
|
||||
},
|
||||
async refreshHelmRepos({commit}){
|
||||
const repos: HelmRepo[] = await repoManager.repositories().catch((error: Error) => {
|
||||
return null;
|
||||
})
|
||||
if(!repos) return false;
|
||||
commit('updateRepos', repos);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const getters: GetterTree<HelmRepoState, any> = {
|
||||
repos: state => state.repos
|
||||
}
|
||||
|
||||
const mutations: MutationTree<HelmRepoState> = {
|
||||
updateRepos(state, repos: HelmRepo[]) {
|
||||
Vue.set(state, 'repos', [...repos])
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: false,
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import * as k8s from "@kubernetes/client-node"
|
||||
import { splitConfig, dumpConfigYaml } from "../../../../main/k8s"
|
||||
|
||||
const state = {
|
||||
availableKubeContexts: []
|
||||
}
|
||||
|
||||
const actions = {
|
||||
reloadAvailableKubeContexts({commit}, file) {
|
||||
let kc = new k8s.KubeConfig();
|
||||
try {
|
||||
kc.loadFromFile(file);
|
||||
} catch (error) {
|
||||
console.error("Failed to read default kubeconfig: " + error.message);
|
||||
}
|
||||
|
||||
// Remove the default setup the client makes if it does not find anything in the default config
|
||||
// See: https://github.com/kubernetes-client/javascript/blob/2fc8fbc956ca89bf425ca3ea045d46ee7b75296b/src/config.ts#L253
|
||||
// It defaults to loadFromClusterAndUser() when no config file can be found
|
||||
if(kc.currentContext === "loaded-context") {
|
||||
kc = new k8s.KubeConfig();
|
||||
}
|
||||
|
||||
commit("saveAvailableKubeContexts", splitConfig(kc))
|
||||
}
|
||||
}
|
||||
|
||||
const getters = {
|
||||
availableKubeContexts: function(state){
|
||||
return state.availableKubeContexts
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
saveAvailableKubeContexts(state, contexts) {
|
||||
state.availableKubeContexts = contexts
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: false,
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { MutationTree, ActionTree, GetterTree } from "vuex"
|
||||
import { workspaceStore, Workspace, WorkspaceData } from "../../../../common/workspace-store"
|
||||
|
||||
export interface WorkspaceState {
|
||||
workspaces: Array<Workspace>;
|
||||
currentWorkspace: Workspace;
|
||||
}
|
||||
|
||||
const state: WorkspaceState = {
|
||||
workspaces: workspaceStore.getAllWorkspaces(),
|
||||
currentWorkspace: workspaceStore.getAllWorkspaces().find((w) => w.id === "default")
|
||||
}
|
||||
|
||||
const actions: ActionTree<WorkspaceState, any> = {
|
||||
}
|
||||
|
||||
const getters: GetterTree<WorkspaceState, any> = {
|
||||
workspaces: state => state.workspaces,
|
||||
currentWorkspace: state => state.currentWorkspace,
|
||||
workspaceById: state => (id: string) => {
|
||||
return state.workspaces.find((ws) => ws.id == id)
|
||||
}
|
||||
}
|
||||
|
||||
const mutations: MutationTree<WorkspaceState> = {
|
||||
setCurrentWorkspace(state, workspace: Workspace) {
|
||||
state.currentWorkspace = workspace
|
||||
},
|
||||
addWorkspace(state, workspace: WorkspaceData) {
|
||||
workspaceStore.storeWorkspace(workspace)
|
||||
state.workspaces = workspaceStore.getAllWorkspaces()
|
||||
},
|
||||
updateWorkspace(state, workspace: WorkspaceData) {
|
||||
workspaceStore.storeWorkspace(workspace)
|
||||
state.workspaces = workspaceStore.getAllWorkspaces()
|
||||
},
|
||||
removeWorkspace(state, workspace: Workspace) {
|
||||
workspaceStore.removeWorkspace(workspace)
|
||||
state.workspaces = workspaceStore.getAllWorkspaces()
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: false,
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// App configuration api
|
||||
import type { IConfigRoutePayload } from "../../../main/routes/config";
|
||||
import { apiBase } from "../index";
|
||||
|
||||
export const configApi = {
|
||||
getConfig() {
|
||||
return apiBase.get<IConfigRoutePayload>("/config")
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { compile } from "path-to-regexp";
|
||||
import { apiHelm } from "../index";
|
||||
import { apiBase } from "../index";
|
||||
import { stringify } from "querystring";
|
||||
import { autobind } from "../../utils";
|
||||
|
||||
@@ -21,7 +21,7 @@ const endpoint = compile(`/v2/charts/:repo?/:name?`) as (params?: {
|
||||
|
||||
export const helmChartsApi = {
|
||||
list() {
|
||||
return apiHelm
|
||||
return apiBase
|
||||
.get<IHelmChartList>(endpoint())
|
||||
.then(data => {
|
||||
return Object
|
||||
@@ -33,7 +33,7 @@ export const helmChartsApi = {
|
||||
|
||||
get(repo: string, name: string, readmeVersion?: string) {
|
||||
const path = endpoint({ repo, name });
|
||||
return apiHelm
|
||||
return apiBase
|
||||
.get<IHelmChartDetails>(path + "?" + stringify({ version: readmeVersion }))
|
||||
.then(data => {
|
||||
const versions = data.versions.map(HelmChart.create);
|
||||
@@ -46,7 +46,7 @@ export const helmChartsApi = {
|
||||
},
|
||||
|
||||
getValues(repo: string, name: string, version: string) {
|
||||
return apiHelm
|
||||
return apiBase
|
||||
.get<string>(`/v2/charts/${repo}/${name}/values?` + stringify({ version }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import jsYaml from "js-yaml";
|
||||
import { compile } from "path-to-regexp";
|
||||
import { autobind, formatDuration } from "../../utils";
|
||||
import capitalize from "lodash/capitalize";
|
||||
import { apiHelm } from "../index";
|
||||
import { apiBase } from "../index";
|
||||
import { helmChartStore } from "../../components/+apps-helm-charts/helm-chart.store";
|
||||
import { ItemObject } from "../../item.store";
|
||||
import { KubeObject } from "../kube-object";
|
||||
@@ -69,14 +69,14 @@ const endpoint = compile(`/v2/releases/:namespace?/:name?`) as (
|
||||
|
||||
export const helmReleasesApi = {
|
||||
list(namespace?: string) {
|
||||
return apiHelm
|
||||
return apiBase
|
||||
.get<HelmRelease[]>(endpoint({ namespace }))
|
||||
.then(releases => releases.map(HelmRelease.create));
|
||||
},
|
||||
|
||||
get(name: string, namespace: string) {
|
||||
const path = endpoint({ name, namespace });
|
||||
return apiHelm.get<IReleaseRawDetails>(path).then(details => {
|
||||
return apiBase.get<IReleaseRawDetails>(path).then(details => {
|
||||
const items: KubeObject[] = JSON.parse(details.resources).items;
|
||||
const resources = items.map(item => KubeObject.create(item));
|
||||
return {
|
||||
@@ -90,34 +90,34 @@ export const helmReleasesApi = {
|
||||
const { repo, ...data } = payload;
|
||||
data.chart = `${repo}/${data.chart}`;
|
||||
data.values = jsYaml.safeLoad(data.values);
|
||||
return apiHelm.post(endpoint(), { data });
|
||||
return apiBase.post(endpoint(), { data });
|
||||
},
|
||||
|
||||
update(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise<IReleaseUpdateDetails> {
|
||||
const { repo, ...data } = payload;
|
||||
data.chart = `${repo}/${data.chart}`;
|
||||
data.values = jsYaml.safeLoad(data.values);
|
||||
return apiHelm.put(endpoint({ name, namespace }), { data });
|
||||
return apiBase.put(endpoint({ name, namespace }), { data });
|
||||
},
|
||||
|
||||
async delete(name: string, namespace: string) {
|
||||
const path = endpoint({ name, namespace });
|
||||
return apiHelm.del(path);
|
||||
return apiBase.del(path);
|
||||
},
|
||||
|
||||
getValues(name: string, namespace: string) {
|
||||
const path = endpoint({ name, namespace }) + "/values";
|
||||
return apiHelm.get<string>(path);
|
||||
return apiBase.get<string>(path);
|
||||
},
|
||||
|
||||
getHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
|
||||
const path = endpoint({ name, namespace }) + "/history";
|
||||
return apiHelm.get(path);
|
||||
return apiBase.get(path);
|
||||
},
|
||||
|
||||
rollback(name: string, namespace: string, revision: number) {
|
||||
const path = endpoint({ name, namespace }) + "/rollback";
|
||||
return apiHelm.put(path, {
|
||||
return apiBase.put(path, {
|
||||
data: {
|
||||
revision: revision
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
// Local express.js endpoints
|
||||
export * from "./config.api"
|
||||
export * from "./cluster.api"
|
||||
export * from "./kubeconfig.api"
|
||||
|
||||
// Kubernetes endpoints
|
||||
// Kubernetes apis
|
||||
// Docs: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/
|
||||
|
||||
export * from "./cluster.api"
|
||||
export * from "./namespaces.api"
|
||||
export * from "./cluster-role.api"
|
||||
export * from "./cluster-role-binding.api"
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
// Kubeconfig api
|
||||
import { apiBase } from "../index";
|
||||
|
||||
export const kubeConfigApi = {
|
||||
getUserConfig() {
|
||||
return apiBase.get("/kubeconfig/user");
|
||||
},
|
||||
|
||||
getServiceAccountConfig(account: string, namespace: string) {
|
||||
return apiBase.get(`/kubeconfig/service-account/${namespace}/${account}`);
|
||||
},
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import moment from "moment";
|
||||
import { apiBase } from "../index";
|
||||
import type { IMetricsQuery } from "../../../main/routes/metrics";
|
||||
import type { IMetricsQuery } from "../../../main/routes/metrics-route";
|
||||
|
||||
export interface IMetrics {
|
||||
status: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import jsYaml from "js-yaml"
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeJsonApiData } from "../kube-json-api";
|
||||
import { apiResourceApplier } from "../index";
|
||||
import { apiBase } from "../index";
|
||||
import { apiManager } from "../api-manager";
|
||||
|
||||
export const resourceApplierApi = {
|
||||
@@ -13,7 +13,7 @@ export const resourceApplierApi = {
|
||||
if (typeof resource === "string") {
|
||||
resource = jsYaml.safeLoad(resource);
|
||||
}
|
||||
return apiResourceApplier
|
||||
return apiBase
|
||||
.post<KubeJsonApiData[]>("/stack", { data: resource })
|
||||
.then(data => {
|
||||
const items = data.map(obj => {
|
||||
|
||||
@@ -24,7 +24,7 @@ export class SelfSubjectRulesReview extends KubeObject {
|
||||
static kind = "SelfSubjectRulesReview"
|
||||
|
||||
spec: {
|
||||
// fixme: add more types from api docs
|
||||
// todo: add more types from api docs
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
import { JsonApi, JsonApiErrorParsed } from "./json-api";
|
||||
import { KubeJsonApi } from "./kube-json-api";
|
||||
import { Notifications } from "../components/notifications";
|
||||
import { apiPrefix, isDevelopment } from "../../common/vars";
|
||||
|
||||
//-- JSON HTTP APIS
|
||||
import { apiKubePrefix, apiPrefix, isDevelopment } from "../../common/vars";
|
||||
|
||||
export const apiBase = new JsonApi({
|
||||
apiBase: apiPrefix,
|
||||
debug: isDevelopment,
|
||||
apiPrefix: apiPrefix.BASE,
|
||||
});
|
||||
export const apiKube = new KubeJsonApi({
|
||||
apiBase: apiKubePrefix,
|
||||
debug: isDevelopment,
|
||||
apiPrefix: apiPrefix.KUBE_BASE,
|
||||
});
|
||||
export const apiHelm = new KubeJsonApi({
|
||||
debug: isDevelopment,
|
||||
apiPrefix: apiPrefix.KUBE_HELM,
|
||||
});
|
||||
export const apiResourceApplier = new KubeJsonApi({
|
||||
debug: isDevelopment,
|
||||
apiPrefix: apiPrefix.KUBE_RESOURCE_APPLIER,
|
||||
});
|
||||
|
||||
// Common handler for HTTP api errors
|
||||
function onApiError(error: JsonApiErrorParsed, res: Response) {
|
||||
export function onApiError(error: JsonApiErrorParsed, res: Response) {
|
||||
switch (res.status) {
|
||||
case 403:
|
||||
error.isUsedForNotification = true;
|
||||
@@ -34,5 +24,3 @@ function onApiError(error: JsonApiErrorParsed, res: Response) {
|
||||
|
||||
apiBase.onError.addListener(onApiError);
|
||||
apiKube.onError.addListener(onApiError);
|
||||
apiHelm.onError.addListener(onApiError);
|
||||
apiResourceApplier.onError.addListener(onApiError);
|
||||
@@ -27,7 +27,7 @@ export interface JsonApiLog {
|
||||
}
|
||||
|
||||
export interface JsonApiConfig {
|
||||
apiPrefix: string;
|
||||
apiBase: string;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
|
||||
}
|
||||
|
||||
protected request<D>(path: string, params?: P, init: RequestInit = {}) {
|
||||
let reqUrl = this.config.apiPrefix + path;
|
||||
let reqUrl = this.config.apiBase + path;
|
||||
const reqInit: RequestInit = { ...this.reqInit, ...init };
|
||||
const { data, query } = params || {} as P;
|
||||
if (data && !reqInit.body) {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
// Parse kube-api path and get api-version, group, etc.
|
||||
|
||||
import type { KubeObject } from "./kube-object";
|
||||
import { splitArray } from "../../common/utils";
|
||||
import { apiManager } from "./api-manager";
|
||||
|
||||
export interface IKubeObjectRef {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
name: string;
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
export interface IKubeApiLinkRef {
|
||||
apiPrefix?: string;
|
||||
@@ -9,13 +19,13 @@ export interface IKubeApiLinkRef {
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
export interface IKubeApiLinkBase extends IKubeApiLinkRef {
|
||||
export interface IKubeApiParsed extends IKubeApiLinkRef {
|
||||
apiBase: string;
|
||||
apiGroup: string;
|
||||
apiVersionWithGroup: string;
|
||||
}
|
||||
|
||||
export function parseApi(path: string): IKubeApiLinkBase {
|
||||
export function parseKubeApi(path: string): IKubeApiParsed {
|
||||
path = new URL(path, location.origin).pathname;
|
||||
const [, prefix, ...parts] = path.split("/");
|
||||
const apiPrefix = `/${prefix}`;
|
||||
@@ -94,7 +104,7 @@ export function parseApi(path: string): IKubeApiLinkBase {
|
||||
};
|
||||
}
|
||||
|
||||
export function createApiLink(ref: IKubeApiLinkRef): string {
|
||||
export function createKubeApiURL(ref: IKubeApiLinkRef): string {
|
||||
const { apiPrefix = "/apis", resource, apiVersion, name } = ref;
|
||||
let { namespace } = ref;
|
||||
if (namespace) {
|
||||
@@ -104,3 +114,36 @@ export function createApiLink(ref: IKubeApiLinkRef): string {
|
||||
.filter(v => v)
|
||||
.join("/")
|
||||
}
|
||||
|
||||
export function lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): string {
|
||||
const {
|
||||
kind, apiVersion, name,
|
||||
namespace = parentObject.getNs()
|
||||
} = ref;
|
||||
|
||||
// search in registered apis by 'kind' & 'apiVersion'
|
||||
const api = apiManager.getApi(api => api.kind === kind && api.apiVersionWithGroup == apiVersion)
|
||||
if (api) {
|
||||
return api.getUrl({ namespace, name })
|
||||
}
|
||||
|
||||
// lookup api by generated resource link
|
||||
const apiPrefixes = ["/apis", "/api"];
|
||||
const resource = kind.toLowerCase() + kind.endsWith("s") ? "es" : "s";
|
||||
for (const apiPrefix of apiPrefixes) {
|
||||
const apiLink = createKubeApiURL({ apiPrefix, apiVersion, name, namespace, resource });
|
||||
if (apiManager.getApi(apiLink)) {
|
||||
return apiLink;
|
||||
}
|
||||
}
|
||||
|
||||
// resolve by kind only (hpa's might use refs to older versions of resources for example)
|
||||
const apiByKind = apiManager.getApi(api => api.kind === kind);
|
||||
if (apiByKind) {
|
||||
return apiByKind.getUrl({ name, namespace })
|
||||
}
|
||||
|
||||
// otherwise generate link with default prefix
|
||||
// resource still might exists in k8s, but api is not registered in the app
|
||||
return createKubeApiURL({ apiVersion, name, namespace, resource })
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IKubeApiLinkBase, parseApi } from "./kube-api-parse";
|
||||
import { IKubeApiParsed, parseKubeApi } from "./kube-api-parse";
|
||||
|
||||
interface KubeApi_Parse_Test {
|
||||
url: string;
|
||||
expected: Required<IKubeApiLinkBase>;
|
||||
expected: Required<IKubeApiParsed>;
|
||||
}
|
||||
|
||||
const tests: KubeApi_Parse_Test[] = [
|
||||
@@ -129,7 +129,7 @@ describe.only("parseApi unit tests", () => {
|
||||
for (const i in tests) {
|
||||
const { url: tUrl, expected:tExpect} = tests[i];
|
||||
test(`test #${parseInt(i)+1}`, () => {
|
||||
expect(parseApi(tUrl)).toStrictEqual(tExpect);
|
||||
expect(parseKubeApi(tUrl)).toStrictEqual(tExpect);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -3,11 +3,11 @@
|
||||
import merge from "lodash/merge"
|
||||
import { stringify } from "querystring";
|
||||
import { IKubeObjectConstructor, KubeObject } from "./kube-object";
|
||||
import { IKubeObjectRef, KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } from "./kube-json-api";
|
||||
import { KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } from "./kube-json-api";
|
||||
import { apiKube } from "./index";
|
||||
import { kubeWatchApi } from "./kube-watch-api";
|
||||
import { apiManager } from "./api-manager";
|
||||
import { createApiLink, parseApi } from "./kube-api-parse";
|
||||
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse";
|
||||
|
||||
export interface IKubeApiOptions<T extends KubeObject> {
|
||||
kind: string; // resource type within api-group, e.g. "Namespace"
|
||||
@@ -26,8 +26,7 @@ export interface IKubeApiQueryParams {
|
||||
}
|
||||
|
||||
export class KubeApi<T extends KubeObject = any> {
|
||||
static parseApi = parseApi;
|
||||
static createLink = createApiLink;
|
||||
static parseApi = parseKubeApi;
|
||||
|
||||
static watchAll(...apis: KubeApi[]) {
|
||||
const disposers = apis.map(api => api.watch());
|
||||
@@ -85,7 +84,7 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
|
||||
getUrl({ name = "", namespace = "" } = {}, query?: Partial<IKubeApiQueryParams>) {
|
||||
const { apiPrefix, apiVersionWithGroup, apiResource } = this;
|
||||
const resourcePath = KubeApi.createLink({
|
||||
const resourcePath = createKubeApiURL({
|
||||
apiPrefix: apiPrefix,
|
||||
apiVersion: apiVersionWithGroup,
|
||||
resource: apiResource,
|
||||
@@ -175,35 +174,4 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
}
|
||||
}
|
||||
|
||||
export function lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): string {
|
||||
const {
|
||||
kind, apiVersion, name,
|
||||
namespace = parentObject.getNs()
|
||||
} = ref;
|
||||
|
||||
// search in registered apis by 'kind' & 'apiVersion'
|
||||
const api = apiManager.getApi(api => api.kind === kind && api.apiVersionWithGroup == apiVersion)
|
||||
if (api) {
|
||||
return api.getUrl({ namespace, name })
|
||||
}
|
||||
|
||||
// lookup api by generated resource link
|
||||
const apiPrefixes = ["/apis", "/api"];
|
||||
const resource = kind.toLowerCase() + kind.endsWith("s") ? "es" : "s";
|
||||
for (const apiPrefix of apiPrefixes) {
|
||||
const apiLink = KubeApi.createLink({ apiPrefix, apiVersion, name, namespace, resource });
|
||||
if (apiManager.getApi(apiLink)) {
|
||||
return apiLink;
|
||||
}
|
||||
}
|
||||
|
||||
// resolve by kind only (hpa's might use refs to older versions of resources for example)
|
||||
const apiByKind = apiManager.getApi(api => api.kind === kind);
|
||||
if (apiByKind) {
|
||||
return apiByKind.getUrl({ name, namespace })
|
||||
}
|
||||
|
||||
// otherwise generate link with default prefix
|
||||
// resource still might exists in k8s, but api is not registered in the app
|
||||
return KubeApi.createLink({ apiVersion, name, namespace, resource })
|
||||
}
|
||||
export * from "./kube-api-parse"
|
||||
@@ -31,13 +31,6 @@ export interface KubeJsonApiData extends JsonApiData {
|
||||
};
|
||||
}
|
||||
|
||||
export interface IKubeObjectRef {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
name: string;
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
export interface KubeJsonApiError extends JsonApiError {
|
||||
code: number;
|
||||
status: string;
|
||||
@@ -49,14 +42,6 @@ export interface KubeJsonApiError extends JsonApiError {
|
||||
};
|
||||
}
|
||||
|
||||
export interface IKubeJsonApiQuery {
|
||||
watch?: any;
|
||||
resourceVersion?: string;
|
||||
timeoutSeconds?: number;
|
||||
limit?: number; // doesn't work with ?watch
|
||||
continue?: string; // might be used with ?limit from second request
|
||||
}
|
||||
|
||||
export class KubeJsonApi extends JsonApi<KubeJsonApiData> {
|
||||
protected parseError(error: KubeJsonApiError | any, res: Response): string[] {
|
||||
const { status, reason, message } = error;
|
||||
|
||||
@@ -6,9 +6,9 @@ import { autobind, EventEmitter } from "../utils";
|
||||
import { KubeJsonApiData } from "./kube-json-api";
|
||||
import type { KubeObjectStore } from "../kube-object.store";
|
||||
import { KubeApi } from "./kube-api";
|
||||
import { configStore } from "../config.store";
|
||||
import { apiManager } from "./api-manager";
|
||||
import { apiPrefix, isDevelopment } from "../../common/vars";
|
||||
import { getHostedCluster } from "../../common/cluster-store";
|
||||
|
||||
export interface IKubeWatchEvent<T = any> {
|
||||
type: "ADDED" | "MODIFIED" | "DELETED";
|
||||
@@ -29,7 +29,6 @@ export interface IKubeWatchRouteQuery {
|
||||
export class KubeWatchApi {
|
||||
protected evtSource: EventSource;
|
||||
protected onData = new EventEmitter<[IKubeWatchEvent]>();
|
||||
protected apiUrl = apiPrefix.BASE + "/watch";
|
||||
protected subscribers = observable.map<KubeApi, number>();
|
||||
protected reconnectTimeoutMs = 5000;
|
||||
protected maxReconnectsOnError = 10;
|
||||
@@ -62,10 +61,10 @@ export class KubeWatchApi {
|
||||
}
|
||||
|
||||
protected getQuery(): Partial<IKubeWatchRouteQuery> {
|
||||
const { isClusterAdmin, allowedNamespaces } = configStore;
|
||||
const { isAdmin, allowedNamespaces } = getHostedCluster()
|
||||
return {
|
||||
api: this.activeApis.map(api => {
|
||||
if (isClusterAdmin) return api.getWatchUrl();
|
||||
if (isAdmin) return api.getWatchUrl();
|
||||
return allowedNamespaces.map(namespace => api.getWatchUrl(namespace))
|
||||
}).flat()
|
||||
}
|
||||
@@ -79,7 +78,7 @@ export class KubeWatchApi {
|
||||
return;
|
||||
}
|
||||
const query = this.getQuery();
|
||||
const apiUrl = this.apiUrl + "?" + stringify(query);
|
||||
const apiUrl = `${apiPrefix}/watch?` + stringify(query);
|
||||
this.evtSource = new EventSource(apiUrl);
|
||||
this.evtSource.onmessage = this.onMessage;
|
||||
this.evtSource.onerror = this.onError;
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { configStore } from "../config.store";
|
||||
|
||||
export function isAllowedResource(resources: string | string[]) {
|
||||
if (!Array.isArray(resources)) {
|
||||
resources = [resources];
|
||||
}
|
||||
const { allowedResources } = configStore;
|
||||
for (const resource of resources) {
|
||||
if (!allowedResources.includes(resource)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { stringify } from "querystring";
|
||||
import { autobind, base64, EventEmitter, interval } from "../utils";
|
||||
import { autobind, base64, EventEmitter } from "../utils";
|
||||
import { WebSocketApi } from "./websocket-api";
|
||||
import { configStore } from "../config.store";
|
||||
import isEqual from "lodash/isEqual"
|
||||
import { apiPrefix, isDevelopment } from "../../common/vars";
|
||||
import { isDevelopment } from "../../common/vars";
|
||||
|
||||
export enum TerminalChannels {
|
||||
STDIN = 0,
|
||||
@@ -25,21 +24,19 @@ enum TerminalColor {
|
||||
NO_COLOR = "\u001b[0m",
|
||||
}
|
||||
|
||||
export interface ITerminalApiOptions {
|
||||
export type TerminalApiQuery = Record<string, string> & {
|
||||
id: string;
|
||||
node?: string;
|
||||
colorTheme?: "light" | "dark";
|
||||
type?: string | "node";
|
||||
}
|
||||
|
||||
export class TerminalApi extends WebSocketApi {
|
||||
protected size: { Width: number; Height: number };
|
||||
protected currentToken: string;
|
||||
protected tokenInterval = interval(60, this.sendNewToken); // refresh every minute
|
||||
|
||||
public onReady = new EventEmitter<[]>();
|
||||
public isReady = false;
|
||||
|
||||
constructor(protected options: ITerminalApiOptions) {
|
||||
constructor(protected options: TerminalApiQuery) {
|
||||
super({
|
||||
logging: isDevelopment,
|
||||
flushOnOpen: false,
|
||||
@@ -47,50 +44,33 @@ export class TerminalApi extends WebSocketApi {
|
||||
});
|
||||
}
|
||||
|
||||
async getUrl(token: string) {
|
||||
const { hostname, protocol } = location;
|
||||
async getUrl() {
|
||||
let { port } = location;
|
||||
const { hostname, protocol } = location;
|
||||
const { id, node } = this.options;
|
||||
const wss = `ws${protocol === "https:" ? "s" : ""}://`;
|
||||
const queryParams = { token, id };
|
||||
const query: TerminalApiQuery = { id };
|
||||
if (port) {
|
||||
port = `:${port}`
|
||||
}
|
||||
if (node) {
|
||||
Object.assign(queryParams, {
|
||||
node: node,
|
||||
type: "node"
|
||||
});
|
||||
query.node = node;
|
||||
query.type = "node";
|
||||
}
|
||||
return `${wss}${hostname}${port}/api?${stringify(queryParams)}`;
|
||||
return `${wss}${hostname}${port}/api?${stringify(query)}`;
|
||||
}
|
||||
|
||||
async connect() {
|
||||
const token = await configStore.getToken();
|
||||
const apiUrl = await this.getUrl(token);
|
||||
const { colorTheme } = this.options;
|
||||
this.emitStatus("Connecting ...", {
|
||||
color: colorTheme == "light" ? TerminalColor.GRAY : TerminalColor.LIGHT_GRAY
|
||||
});
|
||||
const apiUrl = await this.getUrl();
|
||||
this.emitStatus("Connecting ...");
|
||||
this.onData.addListener(this._onReady, { prepend: true });
|
||||
this.currentToken = token;
|
||||
this.tokenInterval.start();
|
||||
return super.connect(apiUrl);
|
||||
}
|
||||
|
||||
@autobind()
|
||||
async sendNewToken() {
|
||||
const token = await configStore.getToken();
|
||||
if (!this.isReady || token == this.currentToken) return;
|
||||
this.sendCommand(token, TerminalChannels.TOKEN);
|
||||
this.currentToken = token;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.socket) return;
|
||||
const exitCode = String.fromCharCode(4); // ctrl+d
|
||||
this.sendCommand(exitCode);
|
||||
this.tokenInterval.stop();
|
||||
setTimeout(() => super.destroy(), 2000);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import get from "lodash/get";
|
||||
import { IKubeObjectMetadata, KubeObject } from "./kube-object";
|
||||
import { KubeObject } from "./kube-object";
|
||||
|
||||
interface IToleration {
|
||||
key?: string;
|
||||
@@ -47,9 +47,7 @@ export interface IAffinity {
|
||||
}
|
||||
|
||||
export class WorkloadKubeObject extends KubeObject {
|
||||
|
||||
// fixme: add type
|
||||
spec: any;
|
||||
spec: any; // todo: add proper types
|
||||
|
||||
getSelectors(): string[] {
|
||||
const selector = this.spec.selector;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import "./components/app.scss"
|
||||
import React from "react";
|
||||
import { render } from "react-dom";
|
||||
import { isMac } from "../common/vars";
|
||||
import { userStore } from "../common/user-store";
|
||||
import { workspaceStore } from "../common/workspace-store";
|
||||
import { clusterStore, getHostedClusterId } from "../common/cluster-store";
|
||||
import { i18nStore } from "./i18n";
|
||||
import { themeStore } from "./theme.store";
|
||||
import { App } from "./components/app";
|
||||
import { LensApp } from "./lens-app";
|
||||
|
||||
type AppComponent = React.ComponentType & {
|
||||
init?(): void;
|
||||
}
|
||||
|
||||
export async function bootstrap(App: AppComponent) {
|
||||
const rootElem = document.getElementById("app")
|
||||
rootElem.classList.toggle("is-mac", isMac);
|
||||
|
||||
// preload common stores
|
||||
await Promise.all([
|
||||
userStore.load(),
|
||||
workspaceStore.load(),
|
||||
clusterStore.load(),
|
||||
i18nStore.init(),
|
||||
themeStore.init(),
|
||||
]);
|
||||
|
||||
// init app's dependencies if any
|
||||
if (App.init) {
|
||||
await App.init();
|
||||
}
|
||||
render(<App/>, rootElem);
|
||||
}
|
||||
|
||||
// run
|
||||
bootstrap(getHostedClusterId() ? App : LensApp);
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from "react";
|
||||
import { Notifications } from "./components/notifications";
|
||||
import { Trans } from "@lingui/macro";
|
||||
|
||||
export function browserCheck() {
|
||||
const ua = window.navigator.userAgent
|
||||
const msie = ua.indexOf('MSIE ') // IE < 11
|
||||
const trident = ua.indexOf('Trident/') // IE 11
|
||||
const edge = ua.indexOf('Edge') // Edge
|
||||
if (msie > 0 || trident > 0 || edge > 0) {
|
||||
Notifications.info(
|
||||
<p>
|
||||
<Trans>
|
||||
<b>Your browser does not support all Lens features. </b>{" "}
|
||||
Please consider using another browser.
|
||||
</Trans>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
|
||||
export const addClusterRoute: RouteProps = {
|
||||
path: "/add-cluster"
|
||||
}
|
||||
|
||||
export const addClusterURL = buildURL(addClusterRoute.path)
|
||||
@@ -0,0 +1,11 @@
|
||||
.AddCluster {
|
||||
.Select {
|
||||
&__control {
|
||||
box-shadow: 0 0 0 1px $borderFaintColor;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
color: $pink-400;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import "./add-cluster.scss"
|
||||
import React, { Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { computed, observable } from "mobx";
|
||||
import { KubeConfig } from "@kubernetes/client-node";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { Select, SelectOption } from "../select";
|
||||
import { Input } from "../input";
|
||||
import { AceEditor } from "../ace-editor";
|
||||
import { Button } from "../button";
|
||||
import { Icon } from "../icon";
|
||||
import { WizardLayout } from "../layout/wizard-layout";
|
||||
import { getKubeConfigLocal, loadConfig, saveConfigToAppFiles, splitConfig, validateConfig } from "../../../common/kube-helpers";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { workspaceStore } from "../../../common/workspace-store";
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { navigate } from "../../navigation";
|
||||
import { userStore } from "../../../common/user-store";
|
||||
import { clusterViewURL } from "../cluster-manager/cluster-view.route";
|
||||
|
||||
@observer
|
||||
export class AddCluster extends React.Component {
|
||||
readonly custom: any = "custom"
|
||||
@observable.ref clusterConfig: KubeConfig;
|
||||
@observable.ref kubeConfig: KubeConfig; // local ~/.kube/config (if available)
|
||||
@observable.ref error: React.ReactNode;
|
||||
|
||||
@observable isWaiting = false
|
||||
@observable showSettings = false
|
||||
@observable proxyServer = ""
|
||||
@observable customConfig = ""
|
||||
|
||||
async componentDidMount() {
|
||||
const kubeConfig: string = await getKubeConfigLocal()
|
||||
if (kubeConfig) {
|
||||
this.kubeConfig = loadConfig(kubeConfig)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
userStore.markNewContextsAsSeen();
|
||||
}
|
||||
|
||||
@computed get isCustom() {
|
||||
return this.clusterConfig === this.custom;
|
||||
}
|
||||
|
||||
@computed get clusterOptions() {
|
||||
const options: SelectOption<KubeConfig>[] = [];
|
||||
if (this.kubeConfig) {
|
||||
splitConfig(this.kubeConfig).forEach(kubeConfig => {
|
||||
const context = kubeConfig.currentContext;
|
||||
const hasContext = clusterStore.hasContext(context);
|
||||
if (!hasContext) {
|
||||
options.push({
|
||||
value: kubeConfig,
|
||||
label: context,
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
options.push({
|
||||
label: <Trans>Custom..</Trans>,
|
||||
value: this.custom,
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
protected formatClusterContextLabel = ({ value, label }: SelectOption<KubeConfig>) => {
|
||||
if (value instanceof KubeConfig) {
|
||||
const context = value.currentContext;
|
||||
const isNew = userStore.newContexts.has(context);
|
||||
const className = `${context} kube-context flex gaps align-center`
|
||||
return (
|
||||
<div className={className}>
|
||||
<span>{context}</span>
|
||||
{isNew && <Icon material="fiber_new"/>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return label;
|
||||
};
|
||||
|
||||
addCluster = async () => {
|
||||
const { clusterConfig, customConfig, proxyServer } = this;
|
||||
const clusterId = uuid();
|
||||
this.isWaiting = true
|
||||
this.error = ""
|
||||
try {
|
||||
const config = this.isCustom ? loadConfig(customConfig) : clusterConfig;
|
||||
if (!config) {
|
||||
this.error = <Trans>Please select kubeconfig</Trans>
|
||||
return;
|
||||
}
|
||||
validateConfig(config);
|
||||
await clusterStore.addCluster({
|
||||
id: clusterId,
|
||||
kubeConfigPath: saveConfigToAppFiles(clusterId, config),
|
||||
workspace: workspaceStore.currentWorkspaceId,
|
||||
contextName: config.currentContext,
|
||||
preferences: {
|
||||
clusterName: config.currentContext,
|
||||
httpsProxy: proxyServer || undefined,
|
||||
},
|
||||
});
|
||||
navigate(clusterViewURL({ params: { clusterId } }))
|
||||
} catch (err) {
|
||||
this.error = String(err);
|
||||
} finally {
|
||||
this.isWaiting = false;
|
||||
}
|
||||
}
|
||||
|
||||
renderInfo() {
|
||||
return (
|
||||
<Fragment>
|
||||
<h2>Clusters associated with Lens</h2>
|
||||
<p>
|
||||
Add clusters by clicking the <span className="text-primary">Add Cluster</span> button.
|
||||
You'll need to obtain a working kubeconfig for the cluster you want to add.
|
||||
</p>
|
||||
<p>
|
||||
Each <a href="https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#context" target="_blank">cluster context</a> is added as a separate item in the
|
||||
left-side cluster menu
|
||||
to allow you to operate easily on multiple clusters and/or contexts.
|
||||
</p>
|
||||
<p>
|
||||
For more information on kubeconfig see <a href="https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/" target="_blank">Kubernetes docs</a>.
|
||||
</p>
|
||||
<p>
|
||||
NOTE: Any manually added cluster is not merged into your kubeconfig file.
|
||||
</p>
|
||||
<p>
|
||||
To see your currently enabled config with <code>kubectl</code>, use <code>kubectl config view --minify --raw</code> command in your terminal.
|
||||
</p>
|
||||
<p>
|
||||
When connecting to a cluster, make sure you have a valid and working kubeconfig for the cluster. Following lists known "gotchas" in some authentication types used in kubeconfig with Lens
|
||||
app.
|
||||
</p>
|
||||
<a href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#option-1-oidc-authenticator" target="_blank">
|
||||
<h3>OIDC (OpenID Connect)</h3>
|
||||
</a>
|
||||
<p>
|
||||
When connecting Lens to OIDC enabled cluster, there's few things you as a user need to take into account.
|
||||
</p>
|
||||
<p><b>Dedicated refresh token</b></p>
|
||||
<p>
|
||||
As Lens app utilized kubeconfig is "disconnected" from your main kubeconfig Lens needs to have it's own refresh token it utilizes.
|
||||
If you share the refresh token with e.g. <code>kubectl</code> who ever uses the token first will invalidate it for the next user.
|
||||
One way to achieve this is with <a href="https://github.com/int128/kubelogin" target="_blank">kubelogin</a> tool by removing the tokens
|
||||
(both <code>id_token</code> and <code>refresh_token</code>) from
|
||||
the config and issuing <code>kubelogin</code> command. That'll take you through the login process and will result you having "dedicated" refresh token.
|
||||
</p>
|
||||
<h3>Exec auth plugins</h3>
|
||||
<p>
|
||||
When using <a href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#configuration" target="_blank">exec auth</a> plugins make sure the paths that are used to call
|
||||
any binaries
|
||||
are full paths as Lens app might not be able to call binaries with relative paths. Make also sure that you pass all needed information either as arguments or env variables in the config,
|
||||
Lens app might not have all login shell env variables set automatically.
|
||||
</p>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<WizardLayout className="AddCluster" infoPanel={this.renderInfo()}>
|
||||
<h2><Trans>Add Cluster</Trans></h2>
|
||||
<p>Choose config:</p>
|
||||
<Select
|
||||
placeholder={<Trans>Select kubeconfig</Trans>}
|
||||
value={this.clusterConfig}
|
||||
options={this.clusterOptions}
|
||||
onChange={({ value }: SelectOption) => this.clusterConfig = value}
|
||||
formatOptionLabel={this.formatClusterContextLabel}
|
||||
id="kubecontext-select"
|
||||
/>
|
||||
<div className="cluster-settings">
|
||||
<a href="#" onClick={() => this.showSettings = !this.showSettings}>
|
||||
<Trans>Proxy settings</Trans>
|
||||
</a>
|
||||
</div>
|
||||
{this.showSettings && (
|
||||
<div className="proxy-settings">
|
||||
<p>HTTP Proxy server. Used for communicating with Kubernetes API.</p>
|
||||
<Input
|
||||
autoFocus
|
||||
value={this.proxyServer}
|
||||
onChange={value => this.proxyServer = value}
|
||||
theme="round-black"
|
||||
/>
|
||||
<small className="hint">
|
||||
{'A HTTP proxy server URL (format: http://<address>:<port>).'}
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
{this.isCustom && (
|
||||
<div className="custom-kubeconfig flex column gaps box grow">
|
||||
<p>Kubeconfig:</p>
|
||||
<AceEditor
|
||||
autoFocus
|
||||
showGutter={false}
|
||||
mode="yaml"
|
||||
value={this.customConfig}
|
||||
onChange={value => this.customConfig = value}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{this.error && (
|
||||
<div className="error">{this.error}</div>
|
||||
)}
|
||||
<div className="actions-panel">
|
||||
<Button
|
||||
primary
|
||||
label={<Trans>Add cluster(s)</Trans>}
|
||||
onClick={this.addCluster}
|
||||
waiting={this.isWaiting}
|
||||
/>
|
||||
</div>
|
||||
</WizardLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./add-cluster"
|
||||
export * from "./add-cluster.route"
|
||||
@@ -1,11 +1,11 @@
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { action, observable, when, IReactionDisposer, reaction } from "mobx";
|
||||
import { action, IReactionDisposer, observable, reaction, when } from "mobx";
|
||||
import { autobind } from "../../utils";
|
||||
import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayload } from "../../api/endpoints/helm-releases.api";
|
||||
import { ItemStore } from "../../item.store";
|
||||
import { configStore } from "../../config.store";
|
||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||
import { Secret } from "../../api/endpoints";
|
||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||
import { getHostedCluster } from "../../../common/cluster-store";
|
||||
|
||||
@autobind()
|
||||
export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
@@ -58,8 +58,8 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
this.isLoading = true;
|
||||
let items;
|
||||
try {
|
||||
const { isClusterAdmin, allowedNamespaces } = configStore;
|
||||
items = await this.loadItems(!isClusterAdmin ? allowedNamespaces : null);
|
||||
const { isAdmin, allowedNamespaces } = getHostedCluster()
|
||||
items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
|
||||
} finally {
|
||||
if (items) {
|
||||
items = this.sortItems(items);
|
||||
@@ -73,8 +73,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
async loadItems(namespaces?: string[]) {
|
||||
if (!namespaces) {
|
||||
return helmReleasesApi.list();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return Promise
|
||||
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
|
||||
.then(items => items.flat());
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { IClusterViewRouteParams } from "../cluster-manager/cluster-view.route";
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
|
||||
export interface IClusterSettingsRouteParams extends IClusterViewRouteParams {
|
||||
}
|
||||
|
||||
export const clusterSettingsRoute: RouteProps = {
|
||||
path: `/cluster/:clusterId/settings`,
|
||||
}
|
||||
|
||||
export const clusterSettingsURL = buildURL<IClusterSettingsRouteParams>(clusterSettingsRoute.path)
|
||||
@@ -0,0 +1,83 @@
|
||||
.ClusterSettings {
|
||||
.WizardLayout {
|
||||
grid-template-columns: unset;
|
||||
grid-template-rows: 76px 1fr;
|
||||
padding: 0;
|
||||
|
||||
.head-col {
|
||||
justify-content: space-between;
|
||||
|
||||
:nth-child(2) {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.content-col {
|
||||
margin: 0;
|
||||
padding-top: $padding * 3;
|
||||
background-color: transparent;
|
||||
|
||||
.SubTitle {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
> div {
|
||||
margin-top: $margin * 5;
|
||||
}
|
||||
|
||||
.admin-note {
|
||||
font-size: small;
|
||||
opacity: 0.5;
|
||||
margin-left: $margin;
|
||||
}
|
||||
|
||||
.button-area {
|
||||
margin-top: $margin * 2;
|
||||
}
|
||||
|
||||
.file-loader {
|
||||
margin-top: $margin * 2;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: smaller;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
p + p, .hint + p {
|
||||
padding-top: $padding;
|
||||
}
|
||||
}
|
||||
|
||||
.status-table {
|
||||
margin: $margin * 3 0;
|
||||
|
||||
.Table {
|
||||
border: 1px solid var(--drawerSubtitleBackground);
|
||||
border-radius: $radius;
|
||||
|
||||
.TableRow {
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: 1px solid var(--drawerSubtitleBackground);
|
||||
}
|
||||
|
||||
.value {
|
||||
flex-grow: 2;
|
||||
word-break: break-word;
|
||||
color: var(--textColorSecondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Input, .Select {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.Select {
|
||||
&__control {
|
||||
box-shadow: 0 0 0 1px $borderFaintColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import "./cluster-settings.scss";
|
||||
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { observer } from "mobx-react";
|
||||
import { Features } from "./features";
|
||||
import { Removal } from "./removal";
|
||||
import { Status } from "./status";
|
||||
import { General } from "./general";
|
||||
import { WizardLayout } from "../layout/wizard-layout";
|
||||
import { ClusterIcon } from "../cluster-icon";
|
||||
import { Icon } from "../icon";
|
||||
import { getMatchedCluster } from "../cluster-manager/cluster-view.route";
|
||||
import { navigate } from "../../navigation";
|
||||
|
||||
@observer
|
||||
export class ClusterSettings extends React.Component {
|
||||
render() {
|
||||
const cluster = getMatchedCluster();
|
||||
if (!cluster) return null;
|
||||
const header = (
|
||||
<>
|
||||
<ClusterIcon
|
||||
cluster={cluster}
|
||||
showErrors={false}
|
||||
showTooltip={false}
|
||||
/>
|
||||
<h2>{cluster.preferences.clusterName}</h2>
|
||||
<Icon material="close" onClick={() => navigate("/")} big/>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<div className="ClusterSettings">
|
||||
<WizardLayout header={header} centered>
|
||||
<Status cluster={cluster}></Status>
|
||||
<General cluster={cluster}></General>
|
||||
<Features cluster={cluster}></Features>
|
||||
<Removal cluster={cluster}></Removal>
|
||||
</WizardLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Cluster } from "../../../../main/cluster";
|
||||
import { Input } from "../../input";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
|
||||
interface Props {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterHomeDirSetting extends React.Component<Props> {
|
||||
@observable directory = this.props.cluster.preferences.terminalCWD || "";
|
||||
|
||||
save = () => {
|
||||
this.props.cluster.preferences.terminalCWD = this.directory;
|
||||
};
|
||||
|
||||
onChange = (value: string) => {
|
||||
this.directory = value;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="Working Directory"/>
|
||||
<p>Terminal working directory.</p>
|
||||
<Input
|
||||
theme="round-black"
|
||||
value={this.directory}
|
||||
onChange={this.onChange}
|
||||
onBlur={this.save}
|
||||
placeholder="$HOME"
|
||||
/>
|
||||
<span className="hint">
|
||||
An explicit start path where the terminal will be launched,{" "}
|
||||
this is used as the current working directory (cwd) for the shell process.
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from "react";
|
||||
import { Cluster } from "../../../../main/cluster";
|
||||
import { FilePicker, OverSizeLimitStyle } from "../../file-picker";
|
||||
import { autobind } from "../../../utils";
|
||||
import { Button } from "../../button";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { ClusterIcon } from "../../cluster-icon";
|
||||
|
||||
enum GeneralInputStatus {
|
||||
CLEAN = "clean",
|
||||
ERROR = "error",
|
||||
}
|
||||
|
||||
interface Props {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterIconSetting extends React.Component<Props> {
|
||||
@observable status = GeneralInputStatus.CLEAN;
|
||||
@observable errorText?: string;
|
||||
|
||||
@autobind()
|
||||
async onIconPick([file]: File[]) {
|
||||
const { cluster } = this.props;
|
||||
try {
|
||||
if (file) {
|
||||
const buf = Buffer.from(await file.arrayBuffer());
|
||||
cluster.preferences.icon = `data:image/jpeg;base64, ${buf.toString('base64')}`;
|
||||
} else {
|
||||
// this has to be done as a seperate branch (and not always) because `cluster`
|
||||
// is observable and triggers an update loop.
|
||||
cluster.preferences.icon = undefined;
|
||||
}
|
||||
} catch (e) {
|
||||
this.errorText = e.toString()
|
||||
this.status = GeneralInputStatus.ERROR
|
||||
}
|
||||
}
|
||||
|
||||
getClearButton() {
|
||||
if (this.props.cluster.preferences.icon) {
|
||||
return <Button tooltip="Revert back to default icon" accent onClick={() => this.onIconPick([])}>Clear</Button>
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const label = (
|
||||
<>
|
||||
<ClusterIcon
|
||||
cluster={this.props.cluster}
|
||||
showErrors={false}
|
||||
showTooltip={false}
|
||||
/>
|
||||
{"Browse for new icon..."}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="Cluster Icon" />
|
||||
<p>Define cluster icon. By default automatically generated.</p>
|
||||
<div className="file-loader">
|
||||
<FilePicker
|
||||
accept="image/*"
|
||||
label={label}
|
||||
onOverSizeLimit={OverSizeLimitStyle.FILTER}
|
||||
handler={this.onIconPick}
|
||||
/>
|
||||
{this.getClearButton()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import { Cluster } from "../../../../main/cluster";
|
||||
import { Input } from "../../input";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { isRequired } from "../../input/input.validators";
|
||||
|
||||
interface Props {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterNameSetting extends React.Component<Props> {
|
||||
@observable name = this.props.cluster.preferences.clusterName || "";
|
||||
|
||||
save = () => {
|
||||
this.props.cluster.preferences.clusterName = this.name;
|
||||
};
|
||||
|
||||
onChange = (value: string) => {
|
||||
this.name = value;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="Cluster Name"/>
|
||||
<p>Define cluster name.</p>
|
||||
<Input
|
||||
theme="round-black"
|
||||
validators={isRequired}
|
||||
value={this.name}
|
||||
onChange={this.onChange}
|
||||
onBlur={this.save}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { prometheusProviders } from "../../../../common/prometheus-providers";
|
||||
import { Cluster } from "../../../../main/cluster";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { Select, SelectOption } from "../../select";
|
||||
import { Input } from "../../input";
|
||||
import { observable, computed } from "mobx";
|
||||
|
||||
const options: SelectOption<string>[] = [
|
||||
{ value: "", label: "Auto detect" },
|
||||
...prometheusProviders.map(pp => ({value: pp.id, label: pp.name}))
|
||||
];
|
||||
|
||||
interface Props {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterPrometheusSetting extends React.Component<Props> {
|
||||
@observable path = "";
|
||||
@observable provider = "";
|
||||
|
||||
@computed get canEditPrometheusPath() {
|
||||
if (this.provider === "" || this.provider === "lens") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { prometheus, prometheusProvider } = this.props.cluster.preferences;
|
||||
if (prometheus) {
|
||||
const prefix = prometheus.prefix || "";
|
||||
this.path = `${prometheus.namespace}/${prometheus.service}:${prometheus.port}${prefix}`;
|
||||
}
|
||||
if (prometheusProvider) {
|
||||
this.provider = prometheusProvider.type;
|
||||
}
|
||||
}
|
||||
|
||||
parsePrometheusPath = () => {
|
||||
if (!this.provider || !this.path) {
|
||||
return null;
|
||||
}
|
||||
const parsed = this.path.split(/\/|:/, 3);
|
||||
const apiPrefix = this.path.substring(parsed.join("/").length);
|
||||
if (!parsed[0] || !parsed[1] || !parsed[2]) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
namespace: parsed[0],
|
||||
service: parsed[1],
|
||||
port: parseInt(parsed[2]),
|
||||
prefix: apiPrefix
|
||||
}
|
||||
}
|
||||
|
||||
onSaveProvider = () => {
|
||||
this.props.cluster.preferences.prometheusProvider = this.provider ?
|
||||
{ type: this.provider } :
|
||||
null;
|
||||
}
|
||||
|
||||
onSavePath = () => {
|
||||
this.props.cluster.preferences.prometheus = this.parsePrometheusPath();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="Prometheus"/>
|
||||
<p>
|
||||
Use pre-installed Prometheus service for metrics. Please refer to the{" "}
|
||||
<a href="https://github.com/lensapp/lens/blob/master/troubleshooting/custom-prometheus.md" target="_blank">guide</a>{" "}
|
||||
for possible configuration changes.
|
||||
</p>
|
||||
<p>Prometheus installation method.</p>
|
||||
<Select
|
||||
value={this.provider}
|
||||
onChange={({value}) => {
|
||||
this.provider = value;
|
||||
this.onSaveProvider();
|
||||
}}
|
||||
options={options}
|
||||
/>
|
||||
<span className="hint">What query format is used to fetch metrics from Prometheus</span>
|
||||
{this.canEditPrometheusPath && (
|
||||
<>
|
||||
<p>Prometheus service address.</p>
|
||||
<Input
|
||||
theme="round-black"
|
||||
value={this.path}
|
||||
onChange={(value) => this.path = value}
|
||||
onBlur={this.onSavePath}
|
||||
placeholder="<namespace>/<service>:<port>"
|
||||
/>
|
||||
<span className="hint">
|
||||
An address to an existing Prometheus installation{" "}
|
||||
({'<namespace>/<service>:<port>'}). Lens tries to auto-detect address if left empty.
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Cluster } from "../../../../main/cluster";
|
||||
import { Input } from "../../input";
|
||||
import { isUrl } from "../../input/input.validators";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
|
||||
interface Props {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterProxySetting extends React.Component<Props> {
|
||||
@observable proxy = this.props.cluster.preferences.httpsProxy || "";
|
||||
|
||||
save = () => {
|
||||
this.props.cluster.preferences.httpsProxy = this.proxy;
|
||||
};
|
||||
|
||||
onChange = (value: string) => {
|
||||
this.proxy = value;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="HTTP Proxy"/>
|
||||
<p>HTTP Proxy server. Used for communicating with Kubernetes API.</p>
|
||||
<Input
|
||||
theme="round-black"
|
||||
value={this.proxy}
|
||||
onChange={this.onChange}
|
||||
onBlur={this.save}
|
||||
placeholder="http://<address>:<port>"
|
||||
validators={isUrl}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { workspacesURL } from "../../+workspaces";
|
||||
import { workspaceStore } from "../../../../common/workspace-store";
|
||||
import { Cluster } from "../../../../main/cluster";
|
||||
import { Select } from "../../../components/select";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
|
||||
interface Props {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterWorkspaceSetting extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="Cluster Workspace"/>
|
||||
<p>
|
||||
Define cluster{" "}
|
||||
<Link to={workspacesURL()}>
|
||||
workspace
|
||||
</Link>.
|
||||
</p>
|
||||
<Select
|
||||
value={this.props.cluster.workspace}
|
||||
onChange={({value}) => this.props.cluster.workspace = value}
|
||||
options={workspaceStore.workspacesList.map(w =>
|
||||
({value: w.id, label: w.name})
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import React from "react";
|
||||
import { observable, reaction, comparer } from "mobx";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { clusterIpc } from "../../../../common/cluster-ipc";
|
||||
import { Cluster } from "../../../../main/cluster";
|
||||
import { Button } from "../../button";
|
||||
import { Notifications } from "../../notifications";
|
||||
import { Spinner } from "../../spinner";
|
||||
|
||||
interface Props {
|
||||
cluster: Cluster
|
||||
feature: string
|
||||
}
|
||||
|
||||
@observer
|
||||
export class InstallFeature extends React.Component<Props> {
|
||||
@observable loading = false;
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this,
|
||||
reaction(() => this.props.cluster.features[this.props.feature], () => {
|
||||
this.loading = false;
|
||||
}, { equals: comparer.structural })
|
||||
);
|
||||
}
|
||||
|
||||
getActionButtons() {
|
||||
const { cluster, feature } = this.props;
|
||||
const features = cluster.features[feature];
|
||||
const disabled = !cluster.isAdmin || this.loading;
|
||||
const loadingIcon = this.loading ? <Spinner/> : null;
|
||||
if (!features) return null;
|
||||
return (
|
||||
<div className="flex gaps align-center">
|
||||
{features.canUpgrade &&
|
||||
<Button
|
||||
primary
|
||||
disabled={disabled}
|
||||
onClick={this.runAction(() =>
|
||||
clusterIpc.upgradeFeature.invokeFromRenderer(cluster.id, feature))
|
||||
}
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
}
|
||||
{features.installed &&
|
||||
<Button
|
||||
accent
|
||||
disabled={disabled}
|
||||
onClick={this.runAction(() =>
|
||||
clusterIpc.uninstallFeature.invokeFromRenderer(cluster.id, feature))
|
||||
}
|
||||
>
|
||||
Uninstall
|
||||
</Button>
|
||||
}
|
||||
{!features.installed && !features.canUpgrade &&
|
||||
<Button
|
||||
primary
|
||||
disabled={disabled}
|
||||
onClick={this.runAction(() =>
|
||||
clusterIpc.installFeature.invokeFromRenderer(cluster.id, feature))
|
||||
}
|
||||
>
|
||||
Install
|
||||
</Button>
|
||||
}
|
||||
{loadingIcon}
|
||||
{!cluster.isAdmin && <span className='admin-note'>Actions can only be performed by admins.</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
runAction(action: () => Promise<any>): () => Promise<void> {
|
||||
return async () => {
|
||||
try {
|
||||
this.loading = true;
|
||||
await action();
|
||||
} catch (err) {
|
||||
Notifications.error(err.toString());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
{this.props.children}
|
||||
<div className="button-area">{this.getActionButtons()}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { observer } from "mobx-react";
|
||||
import { clusterIpc } from "../../../../common/cluster-ipc";
|
||||
import { clusterStore } from "../../../../common/cluster-store";
|
||||
import { Cluster } from "../../../../main/cluster";
|
||||
import { autobind } from "../../../utils";
|
||||
import { Button } from "../../button";
|
||||
import { ConfirmDialog } from "../../confirm-dialog";
|
||||
|
||||
interface Props {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class RemoveClusterButton extends React.Component<Props> {
|
||||
@autobind()
|
||||
confirmRemoveCluster() {
|
||||
const { cluster } = this.props;
|
||||
ConfirmDialog.open({
|
||||
message: <p>Are you sure you want to remove <b>{cluster.preferences.clusterName}</b> from Lens?</p>,
|
||||
labelOk: <Trans>Yes</Trans>,
|
||||
labelCancel: <Trans>No</Trans>,
|
||||
ok: async () => {
|
||||
await clusterStore.removeById(cluster.id);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Button accent onClick={this.confirmRemoveCluster} className="button-area">
|
||||
Remove Cluster
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import { Cluster } from "../../../main/cluster";
|
||||
import { InstallFeature } from "./components/install-feature";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { MetricsFeature } from "../../../features/metrics";
|
||||
import { UserModeFeature } from "../../../features/user-mode";
|
||||
|
||||
interface Props {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
export class Features extends React.Component<Props> {
|
||||
render() {
|
||||
const { cluster } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Features</h2>
|
||||
<InstallFeature cluster={cluster} feature={MetricsFeature.id}>
|
||||
<>
|
||||
<SubTitle title="Metrics"/>
|
||||
<p>
|
||||
Enable timeseries data visualization (Prometheus stack) for your cluster.
|
||||
Install this only if you don't have existing Prometheus stack installed.
|
||||
You can see preview of manifests{" "}
|
||||
<a href="https://github.com/lensapp/lens/tree/master/src/features/metrics" target="_blank">here</a>.
|
||||
</p>
|
||||
</>
|
||||
</InstallFeature>
|
||||
<InstallFeature cluster={cluster} feature={UserModeFeature.id}>
|
||||
<>
|
||||
<SubTitle title="User Mode"/>
|
||||
<p>
|
||||
User Mode feature enables non-admin users to see namespaces they have access to.{" "}
|
||||
This is achieved by configuring RBAC rules so that every authenticated user is granted to list namespaces.
|
||||
</p>
|
||||
</>
|
||||
</InstallFeature>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from "react";
|
||||
import { Cluster } from "../../../main/cluster";
|
||||
import { ClusterNameSetting } from "./components/cluster-name-setting";
|
||||
import { ClusterWorkspaceSetting } from "./components/cluster-workspace-setting";
|
||||
import { ClusterIconSetting } from "./components/cluster-icon-setting";
|
||||
import { ClusterProxySetting } from "./components/cluster-proxy-setting";
|
||||
import { ClusterPrometheusSetting } from "./components/cluster-prometheus-setting";
|
||||
import { ClusterHomeDirSetting } from "./components/cluster-home-dir-setting";
|
||||
|
||||
interface Props {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
export class General extends React.Component<Props> {
|
||||
render() {
|
||||
return <div>
|
||||
<h2>General</h2>
|
||||
<ClusterNameSetting cluster={this.props.cluster} />
|
||||
<ClusterWorkspaceSetting cluster={this.props.cluster} />
|
||||
<ClusterIconSetting cluster={this.props.cluster} />
|
||||
<ClusterProxySetting cluster={this.props.cluster} />
|
||||
<ClusterPrometheusSetting cluster={this.props.cluster} />
|
||||
<ClusterHomeDirSetting cluster={this.props.cluster} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./cluster-settings.route"
|
||||
export * from "./cluster-settings"
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import { Cluster } from "../../../main/cluster";
|
||||
import { RemoveClusterButton } from "./components/remove-cluster-button";
|
||||
|
||||
interface Props {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
export class Removal extends React.Component<Props> {
|
||||
render() {
|
||||
const { cluster } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Removal</h2>
|
||||
<RemoveClusterButton cluster={cluster} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from "react";
|
||||
import { Cluster } from "../../../main/cluster";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { Table, TableCell, TableRow } from "../table";
|
||||
|
||||
interface Props {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
export class Status extends React.Component<Props> {
|
||||
renderStatusRows() {
|
||||
const { cluster } = this.props;
|
||||
const rows = [
|
||||
["Online Status", cluster.online ? "online" : `offline (${cluster.failureReason || "unknown reason"})`],
|
||||
["Distribution", cluster.distribution],
|
||||
["Kerbel Version", cluster.version],
|
||||
["API Address", cluster.apiUrl],
|
||||
["Nodes Count", cluster.nodes || "0"]
|
||||
];
|
||||
return (
|
||||
<Table scrollable={false}>
|
||||
{rows.map(([name, value]) => {
|
||||
return (
|
||||
<TableRow key={name}>
|
||||
<TableCell>{name}</TableCell>
|
||||
<TableCell className="value">{value}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<h2>Status</h2>
|
||||
<SubTitle title="Cluster Status"/>
|
||||
<p>
|
||||
Cluster status information including: detected distribution, kernel version, and online status.
|
||||
</p>
|
||||
<div className="status-table">
|
||||
{this.renderStatusRows()}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -13,17 +13,22 @@ import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import { clusterStore } from "./cluster.store";
|
||||
import { eventStore } from "../+events/event.store";
|
||||
import { isAllowedResource } from "../../api/rbac";
|
||||
import { isAllowedResource } from "../../../common/rbac";
|
||||
|
||||
@observer
|
||||
export class Cluster extends React.Component {
|
||||
private dependentStores = [nodesStore, podsStore];
|
||||
|
||||
private watchers = [
|
||||
interval(60, () => clusterStore.getMetrics()),
|
||||
interval(20, () => eventStore.loadAll())
|
||||
];
|
||||
|
||||
private dependentStores = [nodesStore, podsStore];
|
||||
@computed get isLoaded() {
|
||||
return nodesStore.isLoaded && podsStore.isLoaded
|
||||
}
|
||||
|
||||
// todo: refactor
|
||||
async componentDidMount() {
|
||||
const { dependentStores } = this;
|
||||
if (!isAllowedResource("nodes")) {
|
||||
@@ -46,13 +51,6 @@ export class Cluster extends React.Component {
|
||||
])
|
||||
}
|
||||
|
||||
@computed get isLoaded() {
|
||||
return (
|
||||
nodesStore.isLoaded &&
|
||||
podsStore.isLoaded
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoaded } = this;
|
||||
return (
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./cluster.routes"
|
||||
export * from "./cluster.route"
|
||||
|
||||
@@ -10,7 +10,7 @@ import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config
|
||||
import { configURL } from "./config.route";
|
||||
import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers";
|
||||
import { buildURL } from "../../navigation";
|
||||
import { isAllowedResource } from "../../api/rbac"
|
||||
import { isAllowedResource } from "../../../common/rbac"
|
||||
|
||||
export const certificatesURL = buildURL("/certificates");
|
||||
export const issuersURL = buildURL("/issuers");
|
||||
|
||||
@@ -63,7 +63,7 @@ export class CrdList extends React.Component {
|
||||
if (selectedGroups.length == 1) placeholder = <><Trans>Group</Trans>: {selectedGroups[0]}</>
|
||||
if (selectedGroups.length >= 2) placeholder = <><Trans>Groups</Trans>: {selectedGroups.join(", ")}</>
|
||||
return {
|
||||
// fixme: move to global filters
|
||||
// todo: move to global filters
|
||||
filters: (
|
||||
<Select
|
||||
className="group-select"
|
||||
|
||||
@@ -11,12 +11,10 @@ import { DrawerItem } from "../drawer";
|
||||
import { KubeObjectDetailsProps } from "../kube-object";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { crdStore } from "./crd.store";
|
||||
import { KubeObject } from "../../api/kube-object";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { Input } from "../input";
|
||||
|
||||
// fixme: provide type-safe check for status
|
||||
interface Props extends KubeObjectDetailsProps<KubeObject & { status: any }> {
|
||||
interface Props extends KubeObjectDetailsProps {
|
||||
}
|
||||
|
||||
function CrdColumnValue({ value }: { value: any[] | {} | string }) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import "./events.scss";
|
||||
|
||||
import React from "react";
|
||||
import React, { Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { MainLayout } from "../layout/main-layout";
|
||||
import { eventStore } from "./event.store";
|
||||
import { KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { KubeEvent } from "../../api/endpoints/events.api";
|
||||
import { Tooltip, TooltipContent } from "../tooltip";
|
||||
import { Tooltip } from "../tooltip";
|
||||
import { Link } from "react-router-dom";
|
||||
import { cssNames, IClassName, stopPropagation } from "../../utils";
|
||||
import { Icon } from "../icon";
|
||||
@@ -90,18 +90,14 @@ export class Events extends React.Component<Props> {
|
||||
const detailsUrl = getDetailsUrl(lookupApiLink(involvedObject, event));
|
||||
return [
|
||||
{
|
||||
className: {
|
||||
warning: isWarning
|
||||
},
|
||||
className: { warning: isWarning },
|
||||
title: (
|
||||
<>
|
||||
<Fragment>
|
||||
<span id={tooltipId}>{message}</span>
|
||||
<Tooltip htmlFor={tooltipId} following>
|
||||
<TooltipContent narrow warning={isWarning}>
|
||||
{message}
|
||||
</TooltipContent>
|
||||
<Tooltip targetId={tooltipId} formatters={{ narrow: true, warning: isWarning }}>
|
||||
{message}
|
||||
</Tooltip>
|
||||
</>
|
||||
</Fragment>
|
||||
)
|
||||
},
|
||||
event.getNs(),
|
||||
|
||||
@@ -2,7 +2,6 @@ import "./kube-event-icon.scss";
|
||||
|
||||
import React from "react";
|
||||
import { Icon } from "../icon";
|
||||
import { TooltipContent } from "../tooltip";
|
||||
import { KubeObject } from "../../api/kube-object";
|
||||
import { eventStore } from "./event.store";
|
||||
import { cssNames } from "../../utils";
|
||||
@@ -34,15 +33,17 @@ export class KubeEventIcon extends React.Component<Props> {
|
||||
<Icon
|
||||
material="warning"
|
||||
className={cssNames("KubeEventIcon", { warning: event.isWarning() })}
|
||||
tooltip={(
|
||||
<TooltipContent className="KubeEventTooltip">
|
||||
{event.message}
|
||||
<div className="age">
|
||||
<Icon material="access_time"/>
|
||||
{event.getAge(undefined, undefined, true)}
|
||||
tooltip={{
|
||||
children: (
|
||||
<div className="KubeEventTooltip">
|
||||
<div className="msg">{event.message}</div>
|
||||
<div className="age">
|
||||
<Icon material="access_time"/>
|
||||
{event.getAge(undefined, undefined, true)}
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
)}
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./landing-page.route"
|
||||
export * from "./landing-page"
|
||||
@@ -0,0 +1,8 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
|
||||
export const landingRoute: RouteProps = {
|
||||
path: "/landing"
|
||||
}
|
||||
|
||||
export const landingURL = buildURL(landingRoute.path)
|
||||
@@ -0,0 +1,8 @@
|
||||
.LandingPage {
|
||||
height: 100%;
|
||||
background: #282b2f url(../../components/icon/crane.svg) no-repeat;
|
||||
background-position: 0 35%;
|
||||
background-size: 85%;
|
||||
background-clip: content-box;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import "./landing-page.scss"
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { workspaceStore } from "../../../common/workspace-store";
|
||||
|
||||
@observer
|
||||
export class LandingPage extends React.Component {
|
||||
render() {
|
||||
const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId);
|
||||
const noClustersInScope = !clusters.length;
|
||||
return (
|
||||
<div className="LandingPage flex">
|
||||
{noClustersInScope && (
|
||||
<div className="no-clusters flex column gaps box center">
|
||||
<h1>
|
||||
<Trans>Welcome!</Trans>
|
||||
</h1>
|
||||
<p>
|
||||
<Trans>Get started by associating one or more clusters to Lens.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import { namespaceStore } from "./namespace.store";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { FilterIcon } from "../item-object-list/filter-icon";
|
||||
import { FilterType } from "../item-object-list/page-filters.store";
|
||||
import { isAllowedResource } from "../../api/rbac"
|
||||
|
||||
interface Props extends SelectProps {
|
||||
showIcons?: boolean;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { Namespace, namespacesApi } from "../../api/endpoints";
|
||||
import { IQueryParams, navigation, setQueryParams } from "../../navigation";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { isAllowedResource } from "../..//api/rbac";
|
||||
import { isAllowedResource } from "../../../common/rbac";
|
||||
|
||||
@autobind()
|
||||
export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Ingresses, ingressRoute, ingressURL } from "../+network-ingresses";
|
||||
import { NetworkPolicies, networkPoliciesRoute, networkPoliciesURL } from "../+network-policies";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
import { networkURL } from "./network.route";
|
||||
import { isAllowedResource } from "../../api/rbac";
|
||||
import { isAllowedResource } from "../../../common/rbac";
|
||||
|
||||
interface Props extends RouteComponentProps<{}> {
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { DrawerItem, DrawerItemLabels } from "../drawer";
|
||||
import { Badge } from "../badge";
|
||||
import { TooltipContent } from "../tooltip";
|
||||
import { nodesStore } from "./nodes.store";
|
||||
import { ResourceMetrics } from "../resource-metrics";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
@@ -127,16 +126,17 @@ export class NodeDetails extends React.Component<Props> {
|
||||
key={type}
|
||||
label={type}
|
||||
className={kebabCase(type)}
|
||||
tooltip={
|
||||
<TooltipContent tableView>
|
||||
{Object.entries(condition).map(([key, value]) =>
|
||||
<div key={key} className="flex gaps align-center">
|
||||
<div className="name">{upperFirst(key)}</div>
|
||||
<div className="value">{value}</div>
|
||||
</div>
|
||||
)}
|
||||
</TooltipContent>
|
||||
}
|
||||
tooltip={{
|
||||
formatters: {
|
||||
tableView: true,
|
||||
},
|
||||
children: Object.entries(condition).map(([key, value]) =>
|
||||
<div key={key} className="flex gaps align-center">
|
||||
<div className="name">{upperFirst(key)}</div>
|
||||
<div className="value">{value}</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import "./nodes.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
@@ -15,7 +14,7 @@ import { NodeMenu } from "./node-menu";
|
||||
import { LineProgress } from "../line-progress";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { bytesToUnits } from "../../utils/convertMemory";
|
||||
import { Tooltip, TooltipContent } from "../tooltip";
|
||||
import { Tooltip, TooltipPosition } from "../tooltip";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import upperFirst from "lodash/upperFirst";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
@@ -57,7 +56,10 @@ export class Nodes extends React.Component<Props> {
|
||||
<LineProgress
|
||||
max={cores}
|
||||
value={usage}
|
||||
tooltip={_i18n._(t`CPU:`) + ` ${Math.ceil(usage * 100) / cores}\%, ` + _i18n._(t`cores:`) + ` ${cores}`}
|
||||
tooltip={{
|
||||
position: TooltipPosition.BOTTOM,
|
||||
children: _i18n._(t`CPU:`) + ` ${Math.ceil(usage * 100) / cores}\%, ` + _i18n._(t`cores:`) + ` ${cores}`
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -71,7 +73,10 @@ export class Nodes extends React.Component<Props> {
|
||||
<LineProgress
|
||||
max={capacity}
|
||||
value={usage}
|
||||
tooltip={_i18n._(t`Memory:`) + ` ${Math.ceil(usage * 100 / capacity)}%, ${bytesToUnits(usage, 3)}`}
|
||||
tooltip={{
|
||||
position: TooltipPosition.BOTTOM,
|
||||
children: _i18n._(t`Memory:`) + ` ${Math.ceil(usage * 100 / capacity)}%, ${bytesToUnits(usage, 3)}`
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -85,7 +90,10 @@ export class Nodes extends React.Component<Props> {
|
||||
<LineProgress
|
||||
max={capacity}
|
||||
value={usage}
|
||||
tooltip={_i18n._(t`Disk:`) + ` ${Math.ceil(usage * 100 / capacity)}%, ${bytesToUnits(usage, 3)}`}
|
||||
tooltip={{
|
||||
position: TooltipPosition.BOTTOM,
|
||||
children: _i18n._(t`Disk:`) + ` ${Math.ceil(usage * 100 / capacity)}%, ${bytesToUnits(usage, 3)}`
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -101,15 +109,13 @@ export class Nodes extends React.Component<Props> {
|
||||
return (
|
||||
<div key={type} id={tooltipId} className={cssNames("condition", kebabCase(type))}>
|
||||
{type}
|
||||
<Tooltip htmlFor={tooltipId} following>
|
||||
<TooltipContent tableView>
|
||||
{Object.entries(condition).map(([key, value]) =>
|
||||
<div key={key} className="flex gaps align-center">
|
||||
<div className="name">{upperFirst(key)}</div>
|
||||
<div className="value">{value}</div>
|
||||
</div>
|
||||
)}
|
||||
</TooltipContent>
|
||||
<Tooltip targetId={tooltipId} formatters={{ tableView: true }}>
|
||||
{Object.entries(condition).map(([key, value]) =>
|
||||
<div key={key} className="flex gaps align-center">
|
||||
<div className="name">{upperFirst(key)}</div>
|
||||
<div className="value">{value}</div>
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
</div>)
|
||||
})
|
||||
@@ -162,7 +168,7 @@ export class Nodes extends React.Component<Props> {
|
||||
this.renderDiskUsage(node),
|
||||
<>
|
||||
<span id={tooltipId}>{node.getTaints().length}</span>
|
||||
<Tooltip htmlFor={tooltipId} style={{ whiteSpace: "pre-line" }}>
|
||||
<Tooltip targetId={tooltipId} style={{ whiteSpace: "pre-line" }}>
|
||||
{node.getTaints().map(({ key, effect }) => `${key}: ${effect}`).join("\n")}
|
||||
</Tooltip>
|
||||
</>,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./preferences.route"
|
||||
export * from "./preferences"
|
||||
@@ -0,0 +1,8 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
|
||||
export const preferencesRoute: RouteProps = {
|
||||
path: "/preferences"
|
||||
}
|
||||
|
||||
export const preferencesURL = buildURL(preferencesRoute.path)
|
||||
@@ -0,0 +1,51 @@
|
||||
.Preferences {
|
||||
position: fixed!important; // Allows to cover ClustersMenu
|
||||
z-index: 1;
|
||||
|
||||
.WizardLayout {
|
||||
grid-template-columns: unset;
|
||||
grid-template-rows: 76px 1fr;
|
||||
padding: 0;
|
||||
|
||||
.content-col {
|
||||
background-color: transparent;
|
||||
padding: $padding * 8 0;
|
||||
|
||||
h2 {
|
||||
margin-bottom: $margin * 2;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: $margin * 3;
|
||||
}
|
||||
}
|
||||
|
||||
.repos {
|
||||
position: relative;
|
||||
|
||||
.Badge {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
margin-bottom: 1px;
|
||||
padding: $padding $padding * 2;
|
||||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: -$margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-mac & {
|
||||
.WizardLayout .head-col {
|
||||
padding-top: 32px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.Select {
|
||||
&__control {
|
||||
box-shadow: 0 0 0 1px $borderFaintColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 297 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user