feat: users management

Signed-off-by: Ryan Wang <i@ryanc.cc>
pull/588/head
Ryan Wang 2022-06-23 14:28:29 +08:00
parent 62412028b0
commit 4ef9f0cf5d
11 changed files with 242 additions and 103 deletions

View File

@ -27,10 +27,7 @@ const handleRouteToProfile = () => {
<template>
<div class="flex h-full">
<aside
class="navbar fixed hidden h-full overflow-y-auto md:block"
style="background: #fff"
>
<aside class="navbar fixed hidden h-full overflow-y-auto md:block">
<div class="logo flex justify-center py-5">
<img :src="logo" alt="Halo Logo" style="width: 78px" />
</div>
@ -174,6 +171,7 @@ const handleRouteToProfile = () => {
<style lang="scss">
.navbar {
@apply w-64;
@apply bg-white;
z-index: 999;
box-shadow: 0 4px 4px #f6c6ce;
padding-bottom: 70px;

View File

@ -3,8 +3,9 @@ import { BasicLayout } from "@/layouts";
import { IconUpload, VButton, VTabbar } from "@halo-dev/components";
import { onMounted, provide, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { users } from "@/modules/system/users/users-mock";
import { Starport } from "vue-starport";
import axiosInstance from "@/utils/api-client";
import type { User } from "@/types/extension";
const tabs = [
{
@ -29,15 +30,20 @@ const tabs = [
},
];
const user = ref();
const user = ref<User>();
const route = useRoute();
const { params } = useRoute();
if (route.params.username) {
user.value = users.find((u) => u.username === route.params.username);
} else {
user.value = users[0];
const handleFetchUser = async () => {
try {
const { data } = await axiosInstance.get(
`/api/v1alpha1/users/${params.name}`
);
user.value = data;
} catch (e) {
console.error(e);
}
};
provide("user", user);
@ -48,6 +54,7 @@ const router = useRouter();
// set default active tab
onMounted(() => {
handleFetchUser();
const tab = tabs.find((tab) => tab.routeName === currentRouteName);
activeTab.value = tab ? tab.id : tabs[0].id;
});
@ -62,19 +69,19 @@ const handleTabChange = (id: string) => {
<template>
<BasicLayout>
<header class="bg-white">
<div :class="user.cover" class="h-48 bg-gradient-to-r"></div>
<div class="h-48 bg-gradient-to-r from-gray-800 to-red-500"></div>
<div class="px-4 sm:px-6 lg:px-8">
<div class="-mt-12 flex items-end space-x-5 sm:-mt-16">
<div class="flex">
<Starport
:port="`user-profile-${user.name}`"
class="h-24 w-24 sm:h-32 sm:w-32"
:duration="400"
:port="`user-profile-${user.metadata?.name}`"
class="h-24 w-24 sm:h-32 sm:w-32"
>
<img
:src="user.avatar"
:src="user.spec?.avatar"
alt="Avatar"
class="rounded-full ring-4 ring-white drop-shadow-lg"
class="h-full w-full rounded-full ring-4 ring-white drop-shadow-lg"
/>
</Starport>
</div>
@ -83,7 +90,7 @@ const handleTabChange = (id: string) => {
>
<div class="mt-6 block min-w-0 flex-1">
<h1 class="truncate text-xl font-bold text-gray-900">
<span class="mr-1">{{ user.name }}</span>
<span class="mr-1">{{ user.spec?.displayName }}</span>
</h1>
</div>
<div

View File

@ -11,7 +11,7 @@ import {
} from "@halo-dev/components";
import { useRoute } from "vue-router";
import { computed, ref } from "vue";
import type { Plugin } from "./types";
import type { Plugin } from "@/types/extension";
import axiosInstance from "@/utils/api-client";
const pluginActiveId = ref("detail");
@ -31,7 +31,9 @@ const handleFetchPlugin = async () => {
};
const isStarted = computed(() => {
return plugin.value?.status.phase === "STARTED" && plugin.value?.spec.enabled;
return (
plugin.value?.status?.phase === "STARTED" && plugin.value?.spec.enabled
);
});
const handleChangePluginStatus = async () => {

View File

@ -14,7 +14,7 @@ import {
} from "@halo-dev/components";
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import type { Plugin } from "./types";
import type { Plugin } from "@/types/extension";
import axiosInstance from "@/utils/api-client";
const checkAll = ref(false);
@ -30,7 +30,7 @@ const handleRouteToDetail = (plugin: Plugin) => {
};
function isStarted(plugin: Plugin) {
return plugin.status.phase === "STARTED" && plugin.spec.enabled;
return plugin.status?.phase === "STARTED" && plugin.spec.enabled;
}
const handleFetchPlugins = async () => {

View File

@ -1,47 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PluginDependencies {}
export interface License {
name: string;
url: string;
}
export interface Spec {
displayName: string;
version: string;
author: string;
logo: string;
pluginDependencies: PluginDependencies;
homepage: string;
description: string;
license: License[];
requires: string;
pluginClass: string;
enabled: boolean;
}
export interface Metadata {
name: string;
version: number;
creationTimestamp: Date;
}
export interface Status {
phase: string;
entry?: string;
stylesheet?: string;
}
export interface Plugin {
spec: Spec;
apiVersion: string;
kind: string;
metadata: Metadata;
status: Status;
extensions: Extension[];
}
export interface Extension {
name: string;
fields: string[];
}

View File

@ -1,8 +1,9 @@
<script lang="ts" setup>
import { VButton, VInput, VTextarea } from "@halo-dev/components";
import { inject } from "vue";
import type { User } from "@/types/extension";
const user = inject("user");
const user = inject<User>("user");
</script>
<template>
<form class="space-y-8 divide-y divide-gray-200 sm:space-y-5">
@ -13,7 +14,7 @@ const user = inject("user");
</label>
<div class="mt-1 sm:col-span-3 sm:mt-0">
<div class="flex max-w-lg shadow-sm">
<VInput :modelValue="user.username" />
<VInput :modelValue="user?.metadata?.name" />
</div>
</div>
</div>
@ -23,7 +24,7 @@ const user = inject("user");
</label>
<div class="mt-1 sm:col-span-3 sm:mt-0">
<div class="flex max-w-lg shadow-sm">
<VInput :modelValue="user.name" />
<VInput :modelValue="user?.spec?.displayName" />
</div>
</div>
</div>
@ -34,7 +35,7 @@ const user = inject("user");
</label>
<div class="mt-1 sm:col-span-3 sm:mt-0">
<div class="flex max-w-lg shadow-sm">
<VInput :modelValue="user.email" />
<VInput :modelValue="user?.spec?.email" />
</div>
</div>
</div>
@ -44,7 +45,7 @@ const user = inject("user");
</label>
<div class="mt-1 sm:col-span-3 sm:mt-0">
<div class="flex max-w-lg shadow-sm">
<VTextarea modelValue="Halo" />
<VTextarea :modelValue="user?.spec?.bio" />
</div>
</div>
</div>

View File

@ -2,8 +2,9 @@
import { IconUserSettings, VTag } from "@halo-dev/components";
import { inject } from "vue";
import { useRouter } from "vue-router";
import type { User } from "@/types/extension";
const user = inject("user");
const user = inject<User>("user");
const router = useRouter();
</script>
@ -15,7 +16,7 @@ const router = useRouter();
>
<dt class="text-sm font-medium text-gray-900">显示名称</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ user.name }}
{{ user?.spec?.displayName }}
</dd>
</div>
<div
@ -23,7 +24,7 @@ const router = useRouter();
>
<dt class="text-sm font-medium text-gray-900">用户名</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ user.username }}
{{ user?.metadata?.name }}
</dd>
</div>
<div
@ -31,7 +32,7 @@ const router = useRouter();
>
<dt class="text-sm font-medium text-gray-900">电子邮箱</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ user.email || "未设置" }}
{{ user?.spec?.email || "未设置" }}
</dd>
</div>
<div
@ -43,7 +44,7 @@ const router = useRouter();
<template #leftIcon>
<IconUserSettings />
</template>
{{ user.role }}
{{ user?.metadata?.name }}
</VTag>
</dd>
</div>
@ -52,7 +53,7 @@ const router = useRouter();
>
<dt class="text-sm font-medium text-gray-900">描述</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
Hello Halo
{{ user?.spec?.bio }}
</dd>
</div>
<div
@ -66,7 +67,7 @@ const router = useRouter();
>
<dt class="text-sm font-medium text-gray-900">注册时间</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
2022-01-01 00:00:00
{{ user?.metadata?.creationTimestamp }}
</dd>
</div>
<div
@ -74,7 +75,7 @@ const router = useRouter();
>
<dt class="text-sm font-medium text-gray-900">最近登录时间</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
2022-05-30 12:00:00
{{ user?.metadata?.creationTimestamp }}
</dd>
</div>
</dl>

View File

@ -12,18 +12,24 @@ import {
VSpace,
VTag,
} from "@halo-dev/components";
import { users } from "./users-mock";
import { useRouter } from "vue-router";
import { ref } from "vue";
import { Starport } from "vue-starport";
import axiosInstance from "@/utils/api-client";
import type { User } from "@/types/extension";
const checkAll = ref(false);
const users = ref<User[]>([]);
const router = useRouter();
const handleRouteToDetail = (username: string) => {
router.push({ name: "UserDetail", params: { username } });
const handleFetchUsers = async () => {
try {
const { data } = await axiosInstance.get("/api/v1alpha1/users");
users.value = data;
} catch (e) {
console.error(e);
}
};
handleFetchUsers();
</script>
<template>
<VPageHeader title="用户">
@ -172,7 +178,12 @@ const handleRouteToDetail = (username: string) => {
<li
v-for="(user, index) in users"
:key="index"
@click="handleRouteToDetail(user.username)"
@click="
$router.push({
name: 'UserDetail',
params: { name: user.metadata.name },
})
"
>
<div
:class="{
@ -192,15 +203,15 @@ const handleRouteToDetail = (username: string) => {
type="checkbox"
/>
</div>
<div v-if="user.avatar" class="mr-4">
<div v-if="user.spec.avatar" class="mr-4">
<Starport
:duration="400"
:port="`user-profile-${user.name}`"
:port="`user-profile-${user.metadata.name}`"
class="h-12 w-12"
>
<img
:alt="user.name"
:src="user.avatar"
:alt="user.spec.displayName"
:src="user.spec.avatar"
class="h-full w-full overflow-hidden rounded border bg-white hover:shadow-sm"
/>
</Starport>
@ -208,14 +219,14 @@ const handleRouteToDetail = (username: string) => {
<div class="flex-1">
<div class="flex flex-row items-center">
<span class="mr-2 truncate text-sm font-medium text-gray-900">
{{ user.name }}
{{ user.spec.displayName }}
</span>
<VTag class="sm:hidden">{{ user.role }}</VTag>
<VTag class="sm:hidden">{{ user.metadata.name }}</VTag>
</div>
<div class="mt-1 flex">
<VSpace align="start" direction="column" spacing="xs">
<span class="text-xs text-gray-500">
{{ user.username }}
{{ user.metadata.name }}
</span>
</VSpace>
</div>
@ -224,13 +235,13 @@ const handleRouteToDetail = (username: string) => {
<div
class="inline-flex flex-col flex-col-reverse items-end gap-4 sm:flex-row sm:items-center sm:gap-6"
>
<span class="hidden sm:block">
<div class="hidden items-center sm:flex">
<VTag>
{{ user.role }}
{{ user.metadata.name }}
</VTag>
</span>
</div>
<time class="text-sm text-gray-500" datetime="2020-01-07">
2020-01-07
{{ user.metadata.creationTimestamp }}
</time>
<span class="cursor-pointer">
<IconSettings />

View File

@ -27,7 +27,7 @@ export default definePlugin({
],
},
{
path: ":username",
path: ":name",
component: UserProfileLayout,
children: [
{

166
src/types/extension.d.ts vendored Normal file
View File

@ -0,0 +1,166 @@
export interface License {
name?: string;
url?: string;
}
export interface Metadata {
name: string;
labels?: {
[key: string]: string | null;
} | null;
annotations?: {
[key: string]: string | null;
} | null;
version?: number | null;
creationTimestamp?: string | null;
deletionTimestamp?: string | null;
}
export interface Plugin {
spec: PluginSpec;
status?: PluginStatus;
apiVersion: string;
kind: string;
metadata: Metadata;
extensions: Extension[];
}
export interface Extension {
name: string;
fields: string[];
}
export interface PluginSpec {
displayName?: string;
version?: string;
author?: string;
logo?: string;
pluginDependencies?: {
[key: string]: string;
};
homepage?: string;
description?: string;
license?: License[];
requires?: string;
pluginClass?: string;
enabled?: boolean;
}
export interface PluginStatus {
phase?:
| "CREATED"
| "DISABLED"
| "RESOLVED"
| "STARTED"
| "STOPPED"
| "FAILED";
reason?: string;
message?: string;
lastStartTime?: string;
lastTransitionTime?: string;
entry?: string;
stylesheet?: string;
}
export interface PersonalAccessToken {
spec?: PersonalAccessTokenSpec;
apiVersion: string;
kind: string;
metadata: Metadata;
}
export interface PersonalAccessTokenSpec {
userName?: string;
displayName?: string;
revoked?: boolean;
expiresAt?: string;
scopes?: string;
tokenDigest?: string;
}
export interface RoleBinding {
subjects?: Subject[];
roleRef?: RoleRef;
apiVersion: string;
kind: string;
metadata: Metadata;
}
export interface RoleRef {
kind?: string;
name?: string;
apiGroup?: string;
}
export interface Subject {
kind?: string;
name?: string;
apiGroup?: string;
}
export interface PolicyRule {
apiGroups?: string[];
resources?: string[];
resourceNames?: string[];
nonResourceURLs?: string[];
verbs?: string[];
pluginName?: string;
}
export interface Role {
rules?: PolicyRule[];
apiVersion: string;
kind: string;
metadata: Metadata;
}
export interface LoginHistory {
loginAt: string;
sourceIp: string;
userAgent: string;
successful: boolean;
reason?: string;
}
export interface User {
spec: UserSpec;
status?: UserStatus;
apiVersion: string;
kind: string;
metadata: Metadata;
}
export interface UserSpec {
displayName: string;
avatar?: string;
email: string;
phone?: string;
password?: string;
bio?: string;
registeredAt?: string;
twoFactorAuthEnabled?: boolean;
disabled?: boolean;
loginHistoryLimit?: number;
}
export interface UserStatus {
lastLoginAt?: string;
loginHistories?: LoginHistory[];
}
export interface FileReverseProxyProvider {
directory?: string;
filename?: string;
}
export interface ReverseProxy {
rules?: ReverseProxyRule[];
apiVersion: string;
kind: string;
metadata: Metadata;
}
export interface ReverseProxyRule {
path?: string;
file?: FileReverseProxyProvider;
}

View File

@ -1,7 +1,7 @@
import axios from "axios";
const token =
"eyJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJIYWxvIE93bmVyIiwic3ViIjoiYWRtaW4iLCJleHAiOjE2NTU4OTQxNTQsImlhdCI6MTY1NTgwNzc1NCwic2NvcGUiOlsiUk9MRV9zdXBlci1yb2xlIl19.Gnj0rM8DU2bP1KcgKBUVaKf6zs1pDqGxYvii9zxG4lFv4rVZ_uNGXyfhi9V10vRK0GM4v4NEuMtX9-DYnqAV0wR2JcoFevPrJnHHWsvnFrOQm32qeMpew3PsZ5-YAwi9n8Y9GpAcQz_6aWsEuRwm9w5CC3A67CrYPfCK5qwuR5FFLfiMRqPAqNNuZ4r2IfoSZUvXy4HxhUS-01J2BCqP3-hbdN_-tFHCDxtIO637a51EsCmRItY5wSVNmwYPaPOYV7lbHxzBIKXw5RNXg6SrQCSLTVaaJCXsZjwIirk02RQACr6oqTHPbriBVuu-SIgPXS5PJ9i4VaMCn-z8t-oZlQ";
"eyJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJIYWxvIE93bmVyIiwic3ViIjoiYWRtaW4iLCJleHAiOjE2NTYwNDM3NTYsImlhdCI6MTY1NTk1NzM1Niwic2NvcGUiOlsiUk9MRV9zdXBlci1yb2xlIl19.XHmq5q9-HWkWQsPdcuvldeiKOQxbKHEd9qP33ZWaLSFVgj5D-8QvfLjuLreMWUBLvZXvsqBuDHpib70gO6V2c4VtUbnAQnzr8oQx4E5ypMnWH4Gdbs8UlSpMGjTPzSk-QNFKB48nMo8wgTcq2oyhBsMEIArKFm7v2pa5dSX1LbWTRRpNfJpHPVwrAPzaNkOs_qasS8QzSTHU1C3wCf_A4lEILVhbrHq_mv9yeMQZL0enD-gpbGXEQzHE59zwxFC7kfgb_YhzYZfzuXAv2BIKn4TU14W9aW4HySymsqM0ItO5RT3GmJgurbX9USHhIKfGdTFEG1cfgZ0ZJNNOOLEndA";
const axiosInstance = axios.create({
headers: {