mirror of https://github.com/rancher/dashboard.git
Create component `PageHeaderActionMenu.vue`
Signed-off-by: Phillip Rak <rak.phillip@gmail.com>
This commit is contained in:
parent
cb6c4d1bfd
commit
21050a3695
|
|
@ -21,6 +21,7 @@ import { getApplicableExtensionEnhancements } from '@shell/core/plugin-helpers';
|
|||
import IconOrSvg from '@shell/components/IconOrSvg';
|
||||
import { wait } from '@shell/utils/async';
|
||||
import { authProvidersInfo, parseAuthProvidersInfo } from '@shell/utils/auth';
|
||||
import HeaderPageActionMenu from './HeaderPageActionMenu.vue';
|
||||
|
||||
export default {
|
||||
|
||||
|
|
@ -35,6 +36,7 @@ export default {
|
|||
ClusterProviderIcon,
|
||||
IconOrSvg,
|
||||
AppModal,
|
||||
HeaderPageActionMenu,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
|
@ -72,8 +74,20 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['clusterReady', 'isExplorer', 'isRancher', 'currentCluster',
|
||||
'currentProduct', 'rootProduct', 'backToRancherLink', 'backToRancherGlobalLink', 'pageActions', 'isSingleProduct', 'isRancherInHarvester', 'showTopLevelMenu']),
|
||||
...mapGetters([
|
||||
'clusterReady',
|
||||
'isExplorer',
|
||||
'isRancher',
|
||||
'currentCluster',
|
||||
'currentProduct',
|
||||
'rootProduct',
|
||||
'backToRancherLink',
|
||||
'backToRancherGlobalLink',
|
||||
'pageActions',
|
||||
'isSingleProduct',
|
||||
'isRancherInHarvester',
|
||||
'showTopLevelMenu'
|
||||
]),
|
||||
|
||||
authProviderEnabled() {
|
||||
const authProviders = this.$store.getters['management/all'](MANAGEMENT.AUTH_CONFIG);
|
||||
|
|
@ -294,14 +308,6 @@ export default {
|
|||
this.showSearchModal = false;
|
||||
},
|
||||
|
||||
showPageActionsMenu(show) {
|
||||
this.isPageActionMenuOpen = show;
|
||||
},
|
||||
|
||||
pageAction(action) {
|
||||
this.$store.dispatch('handlePageAction', action);
|
||||
},
|
||||
|
||||
checkClusterName() {
|
||||
this.$nextTick(() => {
|
||||
const el = this.$refs.clusterName;
|
||||
|
|
@ -610,57 +616,7 @@ export default {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showPageActions"
|
||||
id="page-actions"
|
||||
class="actions"
|
||||
>
|
||||
<i
|
||||
data-testid="page-actions-menu"
|
||||
class="icon icon-actions"
|
||||
tabindex="0"
|
||||
@blur="showPageActionsMenu(false)"
|
||||
@click="showPageActionsMenu(true)"
|
||||
@focus.capture="showPageActionsMenu(true)"
|
||||
/>
|
||||
<v-dropdown
|
||||
:triggers="[]"
|
||||
:shown="isPageActionMenuOpen"
|
||||
:autoHide="false"
|
||||
:flip="false"
|
||||
:content="false"
|
||||
:placement="'bottom-end'"
|
||||
:distance="14"
|
||||
:container="'#page-actions'"
|
||||
>
|
||||
<template #popper>
|
||||
<div class="user-menu">
|
||||
<ul
|
||||
data-testid="page-actions-dropdown"
|
||||
class="list-unstyled dropdown"
|
||||
@click.stop="showPageActionsMenu(false)"
|
||||
>
|
||||
<li
|
||||
v-for="(a, i) in pageActions"
|
||||
:key="i"
|
||||
class="user-menu-item"
|
||||
>
|
||||
<a
|
||||
v-if="!a.separator"
|
||||
@click="pageAction(a)"
|
||||
>{{ a.labelKey ? t(a.labelKey) : a.label }}</a>
|
||||
<div
|
||||
v-else
|
||||
class="menu-separator"
|
||||
>
|
||||
<div class="menu-separator-line" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</v-dropdown>
|
||||
</div>
|
||||
<header-page-action-menu v-if="showPageActions" />
|
||||
|
||||
<div class="header-spacer" />
|
||||
<div
|
||||
|
|
@ -1002,20 +958,6 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
||||
> I {
|
||||
font-size: 18px;
|
||||
padding: 6px;
|
||||
&:hover {
|
||||
color: var(--link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-spacer {
|
||||
background-color: var(--header-bg);
|
||||
position: relative;
|
||||
|
|
@ -1113,70 +1055,4 @@ export default {
|
|||
color: var(--secondary);
|
||||
}
|
||||
|
||||
#page-actions {
|
||||
:deep() .v-popper__arrow-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.user-menu {
|
||||
:deep() .v-popper__arrow-container {
|
||||
display: none;
|
||||
}
|
||||
// Remove the default padding on the popup so that the hover on menu items goes full width of the menu
|
||||
:deep() .v-popper__inner {
|
||||
padding: 0 0 10px 0;
|
||||
}
|
||||
|
||||
:deep() .v-popper {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
:deep() .v-popper:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
margin: 0 -10px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-menu-item {
|
||||
a, &.no-link > span {
|
||||
cursor: pointer;
|
||||
padding: 0px 10px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--dropdown-hover-bg);
|
||||
color: var(--dropdown-hover-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// When the menu item is focused, pop the margin and compensate the padding, so that
|
||||
// the focus border appears within the menu
|
||||
&:focus {
|
||||
margin: 0 2px;
|
||||
padding: 10px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-link > span {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
div.menu-separator {
|
||||
cursor: default;
|
||||
padding: 4px 0;
|
||||
|
||||
.menu-separator-line {
|
||||
background-color: var(--border);
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,173 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useClickOutside } from '@shell/composables/useClickOutside';
|
||||
|
||||
const isPageActionMenuOpen = ref(false);
|
||||
|
||||
const showPageActionsMenu = (show: boolean) => {
|
||||
isPageActionMenuOpen.value = show;
|
||||
};
|
||||
|
||||
const store = useStore();
|
||||
const pageActions = computed(() => store.getters.pageActions);
|
||||
const pageAction = (action: string) => {
|
||||
store.dispatch('handlePageAction', action);
|
||||
showPageActionsMenu(false);
|
||||
};
|
||||
|
||||
const target = ref(null);
|
||||
|
||||
useClickOutside(target, () => showPageActionsMenu(false));
|
||||
|
||||
const handleBlurEvent = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Tab') {
|
||||
showPageActionsMenu(false);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dropdown
|
||||
class="actions"
|
||||
:triggers="[]"
|
||||
:shown="isPageActionMenuOpen"
|
||||
:autoHide="false"
|
||||
:flip="false"
|
||||
:placement="'bottom-end'"
|
||||
:distance="-6"
|
||||
>
|
||||
<i
|
||||
data-testid="page-actions-menu"
|
||||
class="icon icon-actions"
|
||||
tabindex="0"
|
||||
@keydown="handleBlurEvent"
|
||||
@click="showPageActionsMenu(true)"
|
||||
@focus.capture="showPageActionsMenu(true)"
|
||||
/>
|
||||
|
||||
<template #popper>
|
||||
<div
|
||||
ref="target"
|
||||
class="user-menu"
|
||||
>
|
||||
<ul
|
||||
data-testid="page-actions-dropdown"
|
||||
class="list-unstyled dropdown"
|
||||
>
|
||||
<li
|
||||
v-for="(a) in pageActions"
|
||||
:key="a.label"
|
||||
class="user-menu-item"
|
||||
>
|
||||
<a
|
||||
v-if="!a.separator"
|
||||
@click="pageAction(a)"
|
||||
>{{ a.labelKey ? t(a.labelKey) : a.label }}</a>
|
||||
<div
|
||||
v-else
|
||||
class="menu-separator"
|
||||
>
|
||||
<div class="menu-separator-line" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</v-dropdown>
|
||||
</template>
|
||||
|
||||
<class lang="scss" scoped>
|
||||
.v-popper__popper {
|
||||
.v-popper__wrapper {
|
||||
.v-popper__arrow-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.v-popper__inner {
|
||||
padding: 10px 0 10px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-actions:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.user-menu-item {
|
||||
a, &.no-link > span {
|
||||
cursor: pointer;
|
||||
padding: 0px 10px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--dropdown-hover-bg);
|
||||
color: var(--dropdown-hover-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// When the menu item is focused, pop the margin and compensate the padding, so that
|
||||
// the focus border appears within the menu
|
||||
&:focus {
|
||||
margin: 0 2px;
|
||||
padding: 10px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-link > span {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
div.menu-separator {
|
||||
cursor: default;
|
||||
padding: 4px 0;
|
||||
|
||||
.menu-separator-line {
|
||||
background-color: var(--border);
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
||||
> I {
|
||||
font-size: 18px;
|
||||
padding: 6px;
|
||||
&:hover {
|
||||
color: var(--link);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.v-popper:focus) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
margin: 0 -10px;
|
||||
}
|
||||
}
|
||||
|
||||
.list-unstyled {
|
||||
li {
|
||||
a {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
&.user-info {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px 20px;
|
||||
border-bottom: solid 1px var(--border);
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</class>
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* useClickOutside is based on onClickOutside from VueUse (https://github.com/vueuse/vueuse/blob/main/packages/core/onClickOutside/index.ts)
|
||||
*
|
||||
* This was originally reimplemented due to a resolution bug found in Yarn 1.x
|
||||
* that involves mapping a html-webpack-plugin-5 alias to html-webpack-plugin.
|
||||
* This bug is unrelated to VueUse, but would break vue/vue-cli as they rely on
|
||||
* an un-aliased version of html-webpack-plugin.
|
||||
*
|
||||
* @note Although there are minor differences between this implementation and
|
||||
* the original, we can easily replace this implementation with VueUse if we
|
||||
* find that we will benefit from importing the library in the future.
|
||||
*/
|
||||
import { onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
export interface OnClickOutsideOptions {
|
||||
/**
|
||||
* List of elements that should not trigger the event.
|
||||
*/
|
||||
ignore?: string[]
|
||||
}
|
||||
|
||||
export const useClickOutside = <T extends OnClickOutsideOptions>(
|
||||
component: any,
|
||||
callback: any,
|
||||
options: T = {} as T,
|
||||
) => {
|
||||
const { ignore = [] } = options;
|
||||
|
||||
let shouldListen = true;
|
||||
|
||||
const shouldIgnore = (event: PointerEvent) => {
|
||||
return ignore.some((target) => {
|
||||
if (typeof target === 'string') {
|
||||
return Array.from(window.document.querySelectorAll(target))
|
||||
.some((el) => el === event.target || event.composedPath().includes(el));
|
||||
} else {
|
||||
const el = target;
|
||||
|
||||
return el && (event.target === el || event.composedPath().includes(el));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const listener = (event: PointerEvent) => {
|
||||
const el = component.value;
|
||||
|
||||
if (!el || el === event.target || event.composedPath().includes(el)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.detail === 0) {
|
||||
shouldListen = !shouldIgnore(event);
|
||||
}
|
||||
|
||||
if (!shouldListen) {
|
||||
shouldListen = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const setShouldListen = (e: any) => {
|
||||
const el = component.value;
|
||||
|
||||
shouldListen = !shouldIgnore(e) && !!(el && !e.composedPath().includes(el));
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('click', listener as any);
|
||||
window.addEventListener('pointerdown', setShouldListen);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('click', listener as any);
|
||||
window.removeEventListener('pointerDown', setShouldListen);
|
||||
});
|
||||
};
|
||||
Loading…
Reference in New Issue