Skip to content

Commit

Permalink
feat: added support for pinning menu items + global keyboard shortcuts
Browse files Browse the repository at this point in the history
  • Loading branch information
unxsist committed Jul 17, 2024
1 parent e3d8c45 commit 2c382c1
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 46 deletions.
35 changes: 19 additions & 16 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import RouterViewport from "@/components/RouterViewport.vue";
import Toaster from "@/components/ui/toast/Toaster.vue";
import CommandPalette from "./components/CommandPalette.vue";
import SettingsContextProvider from "./providers/SettingsContextProvider";
import GlobalShortcutProvider from "./providers/GlobalShortcutProvider";
import ColorSchemeProvider from "./providers/ColorSchemeProvider";
import KubeContextProvider from "./providers/KubeContextProvider";
import CommandPaletteProvider from "./providers/CommandPaletteProvider";
Expand All @@ -20,22 +21,24 @@ import UpdateHandler from "./components/UpdateHandler.vue";
>
<Suspense>
<SettingsContextProvider>
<ColorSchemeProvider>
<DialogProvider>
<KubeContextProvider>
<TabProvider>
<CommandPaletteProvider>
<Navigation />
<RouterViewport />
<Toaster />
<CommandPalette />
<DialogHandler />
<UpdateHandler />
</CommandPaletteProvider>
</TabProvider>
</KubeContextProvider>
</DialogProvider>
</ColorSchemeProvider>
<GlobalShortcutProvider>
<ColorSchemeProvider>
<DialogProvider>
<KubeContextProvider>
<TabProvider>
<CommandPaletteProvider>
<Navigation />
<RouterViewport />
<Toaster />
<CommandPalette />
<DialogHandler />
<UpdateHandler />
</CommandPaletteProvider>
</TabProvider>
</KubeContextProvider>
</DialogProvider>
</ColorSchemeProvider>
</GlobalShortcutProvider>
</SettingsContextProvider>
</Suspense>
</AppLayout>
Expand Down
1 change: 1 addition & 0 deletions src/assets/icons/command_osx.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/icons/pin-line.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/icons/pin.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
113 changes: 89 additions & 24 deletions src/components/Navigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/ui/button";
import { Kubernetes } from "@/services/Kubernetes";
import { KubeContextStateKey } from "@/providers/KubeContextProvider";
import { SettingsContextStateKey } from "@/providers/SettingsContextProvider";
import { GlobalShortcutRegisterShortcutsKey } from "@/providers/GlobalShortcutProvider";
import { injectStrict } from "@/lib/utils";
import { V1APIResource } from "@kubernetes/client-node";
import pluralize from "pluralize";
Expand All @@ -22,6 +24,8 @@ const {
namespace,
authenticated: clusterAuthenticated,
} = injectStrict(KubeContextStateKey);
const { settings } = injectStrict(SettingsContextStateKey);
const refreshShortcuts = injectStrict(GlobalShortcutRegisterShortcutsKey);
interface NavigationGroup {
title: string;
Expand Down Expand Up @@ -75,6 +79,12 @@ const navigationGroups: NavigationGroup[] = [
const clusterResources = ref<Map<string, V1APIResource[]>>(new Map());
const getResourceByName = (resource: string) => {
return Array.from(clusterResources.value.values())
.flat()
.find((r) => r.name === resource);
};
const getCoreResourcesForGroup = (group: NavigationGroup) => {
return Array.from(clusterResources.value.values())
.flat()
Expand Down Expand Up @@ -197,6 +207,22 @@ const quit = () => {
exit(0);
};
const isPinned = (resource: string) => {
return settings.value.pinnedResources.some((r) => r.name === resource);
};
const pinResource = async (resource: { name: string; kind: string }) => {
settings.value.pinnedResources.push(resource);
refreshShortcuts();
};
const unpinResource = (resource: { name: string; kind: string }) => {
settings.value.pinnedResources = settings.value.pinnedResources.filter(
(r) => r.name !== resource.name
);
refreshShortcuts();
};
onMounted(() => {
fetchResources();
Expand Down Expand Up @@ -232,43 +258,82 @@ watch([context, namespace, clusterAuthenticated], () => {
<ContextSwitcher :class="{ 'mt-[30px]': targetPlatform !== 'win32' }" />
<div class="flex w-full flex-grow flex-shrink overflow-hidden">
<ScrollArea class="w-full mt-0 mb-0">
<div><!-- Empty div to fix width and truncation --></div>
<NavigationGroup
v-if="settings.pinnedResources.length > 0"
title="Pinned"
>
<template v-for="(resource, index) in settings.pinnedResources">
<NavigationItem
v-if="getResourceByName(resource.name)"
:key="resource.name"
:icon="formatResourceKind(resource.kind).toLowerCase()"
:pinned="true"
:title="formatResourceKind(resource.kind)"
:shortcut="index > 8 ? undefined : index + 1"
:to="{
path: `/${resource.name}`,
query: { resource: resource.name },
}"
@unpinned="unpinResource(resource)"
/>
</template>
</NavigationGroup>
<NavigationGroup
v-for="group in navigationGroups"
:key="group.title"
:title="group.title"
>
<NavigationItem
:icon="formatResourceKind(resource.kind).toLowerCase()"
:title="formatResourceKind(resource.kind)"
<template
v-for="resource in getCoreResourcesForGroup(group)"
:key="resource.name"
:to="{
path: `/${resource.name}`,
query: { resource: resource.name },
}"
/>
<NavigationItem
:icon="formatResourceKind(resource.kind).toLowerCase()"
:title="formatResourceKind(resource.kind)"
>
<NavigationItem
v-if="!isPinned(resource.name)"
:icon="formatResourceKind(resource.kind).toLowerCase()"
:title="formatResourceKind(resource.kind)"
:to="{
path: `/${resource.name}`,
query: { resource: resource.name },
}"
@pinned="pinResource(resource)"
@unpinned="unpinResource(resource)"
/>
</template>
<template
v-for="resource in getApiResourcesForGroup(group)"
:key="resource.name"
:to="{
path: `/${resource.name}`,
query: { resource: resource.name },
}"
/>
>
<NavigationItem
v-if="!isPinned(resource.name)"
:icon="formatResourceKind(resource.kind).toLowerCase()"
:title="formatResourceKind(resource.kind)"
:to="{
path: `/${resource.name}`,
query: { resource: resource.name },
}"
@pinned="pinResource(resource)"
@unpinned="unpinResource(resource)"
/>
</template>
</NavigationGroup>
<NavigationGroup title="Other">
<NavigationItem
:icon="formatResourceKind(resource.kind).toLowerCase()"
:title="formatResourceKind(resource.kind)"
<template
v-for="resource in getOtherResources()"
:key="resource.name"
:to="{
path: `/${resource.name}`,
query: { resource: resource.kind },
}"
/>
>
<NavigationItem
v-if="!isPinned(resource.name)"
:icon="formatResourceKind(resource.kind).toLowerCase()"
:title="formatResourceKind(resource.kind)"
:to="{
path: `/${resource.name}`,
query: { resource: resource.kind },
}"
@pinned="pinResource(resource)"
@unpinned="unpinResource(resource)"
/>
</template>
</NavigationGroup>
</ScrollArea>
</div>
Expand Down
60 changes: 54 additions & 6 deletions src/components/NavigationItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,26 @@ import {
RegisterCommandStateKey,
UnregisterCommandStateKey,
} from "@/providers/CommandPaletteProvider";
import PinIcon from "@/assets/icons/pin.svg";
import PinLineIcon from "@/assets/icons/pin-line.svg";
import { type } from "@tauri-apps/api/os";
const router = useRouter();
const props = defineProps<{
icon: string;
title: string;
to: RouteLocationRaw;
}>();
const os = ref("");
const props = withDefaults(
defineProps<{
icon: string;
title: string;
to: RouteLocationRaw;
pinned?: boolean;
shortcut?: number | undefined;
}>(),
{
pinned: false,
shortcut: undefined,
}
);
const registerCommand = injectStrict(RegisterCommandStateKey);
const unregisterCommand = injectStrict(UnregisterCommandStateKey);
Expand All @@ -28,6 +40,10 @@ registerCommand({
},
});
onMounted(async () => {
os.value = await type();
});
onUnmounted(() => {
unregisterCommand(commandId);
});
Expand All @@ -36,9 +52,41 @@ onUnmounted(() => {
<router-link
:to="props.to"
active-class="bg-background border !border-border text-primary"
class="border border-transparent flex items-center font-semibold rounded-l-lg border-r-0 px-2 py-1 text-[#7a7a7a] cursor-pointer transition-all hover:bg-background hover:text-primary"
class="group/main border border-transparent flex items-center font-semibold rounded-l-lg border-r-0 px-2 py-1 text-[#7a7a7a] cursor-pointer transition-all hover:bg-background hover:text-primary"
>
<NavigationItemIcon :name="props.icon" />
<span class="w-[135px] mx-3 truncate" :title="title">{{ title }}</span>
<div class="block h-full group-hover/main:hidden">
<span
v-if="props.shortcut"
class="text-xxs text-[#7a7a7a] whitespace-nowrap"
>
<span>
{{ os === "Darwin" ? "⌘" : "Ctrl" }}{{ os === "Windows_NT" ? "+" : ""
}}{{ props.shortcut }}
</span>
</span>
</div>
<div class="hidden group-hover/main:block">
<div
class="group/pin"
@click.prevent="$emit(pinned ? 'unpinned' : 'pinned')"
>
<PinLineIcon
class="h-4"
:class="{
'block group-hover/pin:hidden': !pinned,
'hidden group-hover/pin:block': pinned,
}"
/>
<PinIcon
class="h-4"
:class="{
'hidden group-hover/pin:block': !pinned,
'block group-hover/pin:hidden': pinned,
}"
/>
</div>
</div>
</router-link>
</template>
54 changes: 54 additions & 0 deletions src/providers/GlobalShortcutProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { SettingsContextStateKey } from "@/providers/SettingsContextProvider";
import { injectStrict } from "@/lib/utils";
import {
register,
unregister,
unregisterAll,
} from "@tauri-apps/api/globalShortcut";
import { useRouter } from "vue-router";

export const GlobalShortcutRegisterShortcutsKey: InjectionKey<() => void> =
Symbol("GlobalShortcutRegisterShortcuts");

export default {
name: "GlobalShortcutProvider",
async setup() {
const state = reactive({
shortcuts: [] as string[],
});

const { settings } = injectStrict(SettingsContextStateKey);
const router = useRouter();

const registerShortcuts = async () => {
await clearShortcuts();

settings.value.pinnedResources.forEach((resource, index) => {
if (index > 8) return;

register("CommandOrControl+" + (index + 1), () => {
router.push({
path: `/${resource.name}`,
query: { resource: resource.name },
});
});
state.shortcuts.push("CommandOrControl+" + (index + 1));
});
};

provide(GlobalShortcutRegisterShortcutsKey, registerShortcuts);

const clearShortcuts = async () => {
state.shortcuts.forEach((shortcut) => {
unregister(shortcut);
});
state.shortcuts = [];
};

unregisterAll();
registerShortcuts();
},
render(): any {
return this.$slots.default();
},
};
2 changes: 2 additions & 0 deletions src/providers/SettingsContextProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface SettingsContextState {
kubeConfigs: string[];
contextSettings: ContextSettings[];
collapsedNavigationGroups: string[];
pinnedResources: { name: string; kind: string }[];
appearance: {
colorScheme: "auto" | "light" | "dark";
};
Expand Down Expand Up @@ -59,6 +60,7 @@ export default {
kubeConfigs: [],
contextSettings: [],
collapsedNavigationGroups: [],
pinnedResources: [],
appearance: {
colorScheme: "auto",
},
Expand Down

0 comments on commit 2c382c1

Please sign in to comment.