chore: Typescript: Rewrite Top (#21632)
This commit is contained in:
parent
b8563f2d4a
commit
b89be8fd8b
11 changed files with 239 additions and 180 deletions
|
|
@ -24,14 +24,15 @@
|
|||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import "@h2d2/shopicons/es/regular/home";
|
||||
import "@h2d2/shopicons/es/regular/settings";
|
||||
import Navigation from "./Navigation.vue";
|
||||
import collector from "@/mixins/collector";
|
||||
import store from "@/store";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: "TopHeader",
|
||||
components: {
|
||||
TopNavigation: Navigation,
|
||||
|
|
@ -47,7 +48,7 @@ export default {
|
|||
return { vehicleLogins, ...this.collectProps(Navigation, store.state) };
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
import Navigation from "./Navigation.vue";
|
||||
|
||||
export default {
|
||||
title: "Top/Navigation",
|
||||
component: Navigation,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
vehicleLogins: { control: "object" },
|
||||
sponsor: { control: "object" },
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args) => ({
|
||||
components: { Navigation },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Navigation v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Standard = Template.bind({});
|
||||
Standard.args = {};
|
||||
|
||||
export const VehicleLogins = Template.bind({});
|
||||
VehicleLogins.args = {
|
||||
vehicleLogins: {
|
||||
"Mercedes EQS": {
|
||||
authenticated: true,
|
||||
uri: "https://login-provider-a.test/",
|
||||
},
|
||||
"Nissan Leaf Pro": {
|
||||
authenticated: true,
|
||||
uri: "https://login-provider-b.test/",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const PendingVehicleLogins = Template.bind({});
|
||||
PendingVehicleLogins.args = {
|
||||
vehicleLogins: {
|
||||
"Mercedes EQS": {
|
||||
authenticated: true,
|
||||
uri: "https://login-provider-a.test/",
|
||||
},
|
||||
"Nissan Leaf Pro": {
|
||||
authenticated: false,
|
||||
uri: "https://login-provider-b.test/",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const TokenExpires = Template.bind({});
|
||||
TokenExpires.args = {
|
||||
sponsor: { expiresSoon: true },
|
||||
};
|
||||
62
assets/js/components/Top/Navigation.stories.ts
Normal file
62
assets/js/components/Top/Navigation.stories.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import Navigation from "./Navigation.vue";
|
||||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
export default {
|
||||
title: "Top/Navigation",
|
||||
component: Navigation,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
vehicleLogins: { control: "object" },
|
||||
sponsor: { control: "object" },
|
||||
},
|
||||
} as Meta<typeof Navigation>;
|
||||
|
||||
const Template: StoryFn<typeof Navigation> = (args) => ({
|
||||
components: { Navigation },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Navigation v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Standard = Template.bind({});
|
||||
Standard.args = {};
|
||||
|
||||
export const VehicleLogins = Template.bind({});
|
||||
VehicleLogins.args = {
|
||||
vehicleLogins: {
|
||||
"Mercedes EQS": {
|
||||
authenticated: true,
|
||||
uri: "https://login-provider-a.test/",
|
||||
},
|
||||
"Nissan Leaf Pro": {
|
||||
authenticated: true,
|
||||
uri: "https://login-provider-b.test/",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const PendingVehicleLogins = Template.bind({});
|
||||
PendingVehicleLogins.args = {
|
||||
vehicleLogins: {
|
||||
"Mercedes EQS": {
|
||||
authenticated: true,
|
||||
uri: "https://login-provider-a.test/",
|
||||
},
|
||||
"Nissan Leaf Pro": {
|
||||
authenticated: false,
|
||||
uri: "https://login-provider-b.test/",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const TokenExpires = Template.bind({});
|
||||
TokenExpires.args = {
|
||||
sponsor: {
|
||||
name: "Sponsor",
|
||||
expiresAt: new Date().toISOString(),
|
||||
expiresSoon: true,
|
||||
},
|
||||
};
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
import Dropdown from "bootstrap/js/dist/dropdown";
|
||||
import "@h2d2/shopicons/es/regular/gift";
|
||||
|
|
@ -135,30 +135,34 @@ import { logout, isLoggedIn, openLoginModal } from "../Auth/auth";
|
|||
import baseAPI from "./baseapi";
|
||||
import { isApp, sendToApp } from "@/utils/native";
|
||||
import { isUserConfigError } from "@/utils/fatal";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import type { FatalError, Sponsor, VehicleLogins } from "@/types/evcc";
|
||||
import type { Provider as Provider } from "./types";
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: "TopNavigation",
|
||||
mixins: [collector],
|
||||
props: {
|
||||
vehicleLogins: {
|
||||
type: Object,
|
||||
type: Object as PropType<VehicleLogins>,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
sponsor: {
|
||||
type: Object,
|
||||
type: Object as PropType<Sponsor>,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
forecast: Object,
|
||||
battery: Array,
|
||||
fatal: Object,
|
||||
fatal: Object as PropType<FatalError>,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isApp: isApp(),
|
||||
dropdown: null as Dropdown | null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -168,7 +172,7 @@ export default {
|
|||
logoutCount() {
|
||||
return this.providerLogins.filter((login) => !login.loggedIn).length;
|
||||
},
|
||||
providerLogins() {
|
||||
providerLogins(): Provider[] {
|
||||
return Object.entries(this.vehicleLogins).map(([k, v]) => ({
|
||||
title: k,
|
||||
loggedIn: v.authenticated,
|
||||
|
|
@ -205,13 +209,15 @@ export default {
|
|||
if (!$el) {
|
||||
return;
|
||||
}
|
||||
this.dropdown = new Dropdown(document.getElementById("topNavigatonDropdown"));
|
||||
this.dropdown = new Dropdown(
|
||||
document.getElementById("topNavigatonDropdown") as HTMLElement
|
||||
);
|
||||
},
|
||||
unmounted() {
|
||||
this.dropdown?.dispose();
|
||||
},
|
||||
methods: {
|
||||
async handleProviderAuthorization(provider) {
|
||||
async handleProviderAuthorization(provider: Provider) {
|
||||
if (!provider.loggedIn) {
|
||||
baseAPI.post(provider.loginPath).then(function (response) {
|
||||
window.location.href = response.data.loginUri;
|
||||
|
|
@ -221,21 +227,27 @@ export default {
|
|||
}
|
||||
},
|
||||
openSettingsModal() {
|
||||
const modal = Modal.getOrCreateInstance(document.getElementById("globalSettingsModal"));
|
||||
const modal = Modal.getOrCreateInstance(
|
||||
document.getElementById("globalSettingsModal") as HTMLElement
|
||||
);
|
||||
modal.show();
|
||||
},
|
||||
openHelpModal() {
|
||||
const modal = Modal.getOrCreateInstance(document.getElementById("helpModal"));
|
||||
const modal = Modal.getOrCreateInstance(
|
||||
document.getElementById("helpModal") as HTMLElement
|
||||
);
|
||||
modal.show();
|
||||
},
|
||||
openBatterySettingsModal() {
|
||||
const modal = Modal.getOrCreateInstance(
|
||||
document.getElementById("batterySettingsModal")
|
||||
document.getElementById("batterySettingsModal") as HTMLElement
|
||||
);
|
||||
modal.show();
|
||||
},
|
||||
openForecastModal() {
|
||||
const modal = Modal.getOrCreateInstance(document.getElementById("forecastModal"));
|
||||
const modal = Modal.getOrCreateInstance(
|
||||
document.getElementById("forecastModal") as HTMLElement
|
||||
);
|
||||
modal.show();
|
||||
},
|
||||
openNativeSettings() {
|
||||
|
|
@ -249,7 +261,7 @@ export default {
|
|||
this.$router.push({ path: "/" });
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.menu-button {
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
import Notifications from "./Notifications.vue";
|
||||
|
||||
const timeAgo = (hours = 0, minutes = 0, seconds = 0) => {
|
||||
const date = new Date();
|
||||
date.setHours(date.getHours() - hours);
|
||||
date.setMinutes(date.getMinutes() - minutes);
|
||||
date.setSeconds(date.getSeconds() - seconds);
|
||||
return date;
|
||||
};
|
||||
|
||||
const notificationsData = [
|
||||
{
|
||||
message: "Server unavailable",
|
||||
level: "error",
|
||||
time: timeAgo(),
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
message: "charger out of sync: expected disabled, got enabled",
|
||||
level: "warn",
|
||||
count: 4,
|
||||
time: timeAgo(0, 0, 42),
|
||||
},
|
||||
{
|
||||
message: "Sponsortoken: x509: certificate has expired",
|
||||
level: "error",
|
||||
count: 1,
|
||||
time: timeAgo(1, 12, 44),
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Block device full: can not write to /tmp/evcc/foobarloremawefhwuiehfwuiehfwiauhefjkajowaeigjwalvmoweivwail",
|
||||
level: "error",
|
||||
count: 1,
|
||||
time: timeAgo(1, 12, 44),
|
||||
},
|
||||
{
|
||||
message: "charger out of sync: expected disabled, got enabled",
|
||||
level: "warn",
|
||||
count: 4,
|
||||
time: timeAgo(1, 22, 0),
|
||||
},
|
||||
{
|
||||
message: "vehicle remote charge start: invalid character '<' looking for beginning of value",
|
||||
level: "warn",
|
||||
count: 3,
|
||||
time: timeAgo(4, 2, 0),
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Amet irure quis incididunt voluptate esse. Commodo ea sunt est ipsum tempor nisi laboris voluptate labore elit laborum. Ex irure commodo reprehenderit consequat consequat do ad tempor aliquip deserunt eu. Laboris minim nostrud quis nisi. Dolor occaecat reprehenderit velit dolore exercitation cupidatat et voluptate. Nulla pariatur deserunt esse minim nisi nisi nulla. Sit eiusmod do incididunt sint minim pariatur aute.",
|
||||
level: "warn",
|
||||
count: 1,
|
||||
time: timeAgo(5, 2, 44),
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
title: "Top/Notifications",
|
||||
component: Notifications,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
notifications: { control: "object" },
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args) => ({
|
||||
components: { Notifications },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Notifications v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
notifications: notificationsData,
|
||||
};
|
||||
90
assets/js/components/Top/Notifications.stories.ts
Normal file
90
assets/js/components/Top/Notifications.stories.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import Notifications from "./Notifications.vue";
|
||||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
import type { Notification } from "./types";
|
||||
|
||||
const timeAgo = (hours = 0, minutes = 0, seconds = 0) => {
|
||||
const date = new Date();
|
||||
date.setHours(date.getHours() - hours);
|
||||
date.setMinutes(date.getMinutes() - minutes);
|
||||
date.setSeconds(date.getSeconds() - seconds);
|
||||
return date;
|
||||
};
|
||||
|
||||
const notificationsData: Notification[] = [
|
||||
{
|
||||
message: "Server unavailable",
|
||||
level: "error",
|
||||
time: timeAgo(),
|
||||
count: 1,
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message: "charger out of sync: expected disabled, got enabled",
|
||||
level: "warn",
|
||||
count: 4,
|
||||
time: timeAgo(0, 0, 42),
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message: "Sponsortoken: x509: certificate has expired",
|
||||
level: "error",
|
||||
count: 1,
|
||||
time: timeAgo(1, 12, 44),
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Block device full: can not write to /tmp/evcc/foobarloremawefhwuiehfwuiehfwiauhefjkajowaeigjwalvmoweivwail",
|
||||
level: "error",
|
||||
count: 1,
|
||||
time: timeAgo(1, 12, 44),
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message: "charger out of sync: expected disabled, got enabled",
|
||||
level: "warn",
|
||||
count: 4,
|
||||
time: timeAgo(1, 22, 0),
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message:
|
||||
"vehicle remote charge start: invalid character '<' looking for beginning of value",
|
||||
level: "warn",
|
||||
count: 3,
|
||||
time: timeAgo(4, 2, 0),
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Amet irure quis incididunt voluptate esse. Commodo ea sunt est ipsum tempor nisi laboris voluptate labore elit laborum. Ex irure commodo reprehenderit consequat consequat do ad tempor aliquip deserunt eu. Laboris minim nostrud quis nisi. Dolor occaecat reprehenderit velit dolore exercitation cupidatat et voluptate. Nulla pariatur deserunt esse minim nisi nisi nulla. Sit eiusmod do incididunt sint minim pariatur aute.",
|
||||
level: "warn",
|
||||
count: 1,
|
||||
time: timeAgo(5, 2, 44),
|
||||
lp: 1,
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
title: "Top/Notifications",
|
||||
component: Notifications,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
notifications: { control: "object" },
|
||||
},
|
||||
} as Meta<typeof Notifications>;
|
||||
|
||||
const Template: StoryFn<typeof Notifications> = (args) => ({
|
||||
components: { Notifications },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Notifications v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
notifications: notificationsData,
|
||||
};
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
class="d-flex justify-content-end mt-3"
|
||||
:title="fmtAbsoluteDate(msg.time)"
|
||||
>
|
||||
{{ fmtTimeAgo(msg.time - new Date()) }}
|
||||
{{ fmtTimeAgo(msg.time.getTime() - new Date().getTime()) }}
|
||||
</small>
|
||||
<p class="d-flex align-items-baseline">
|
||||
<shopicon-regular-exclamationtriangle
|
||||
|
|
@ -86,16 +86,24 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import "@h2d2/shopicons/es/regular/exclamationtriangle";
|
||||
import formatter from "@/mixins/formatter";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import type { Notification } from "./types";
|
||||
import type { Timeout } from "@/types/evcc";
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: "Notifications",
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
notifications: Array,
|
||||
loadpointTitles: Array,
|
||||
notifications: { type: Array as PropType<Notification[]>, default: () => [] },
|
||||
loadpointTitles: { type: Array as PropType<string[]>, default: () => [] },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
interval: null as Timeout,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
iconVisible() {
|
||||
|
|
@ -113,10 +121,12 @@ export default {
|
|||
}, 10 * 1000);
|
||||
},
|
||||
unmounted() {
|
||||
clearTimeout(this.interval);
|
||||
if (this.interval) {
|
||||
clearTimeout(this.interval);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
message({ message, lp }) {
|
||||
message({ message, lp }: { message: string | string[]; lp?: number }) {
|
||||
const lines = Array.isArray(message) ? message : [message];
|
||||
if (lp) {
|
||||
// add loadpoint title to first line
|
||||
|
|
@ -128,5 +138,5 @@ export default {
|
|||
window.app?.clear();
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
import axios from "axios";
|
||||
|
||||
const { protocol, hostname, port, pathname } = window.location;
|
||||
|
||||
const baseAPI = axios.create({
|
||||
baseURL: protocol + "//" + hostname + (port ? ":" + port : "") + pathname,
|
||||
});
|
||||
|
||||
// global error handling
|
||||
baseAPI.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
const url = error.config.baseURL + error.config.url;
|
||||
const message = `${error.message}: API request failed ${url}`;
|
||||
window.app.raise({ message });
|
||||
}
|
||||
);
|
||||
|
||||
export default baseAPI;
|
||||
19
assets/js/components/Top/baseapi.ts
Normal file
19
assets/js/components/Top/baseapi.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import axios from "axios";
|
||||
|
||||
const { protocol, hostname, port, pathname } = window.location;
|
||||
|
||||
const baseAPI = axios.create({
|
||||
baseURL: protocol + "//" + hostname + (port ? ":" + port : "") + pathname,
|
||||
});
|
||||
|
||||
// global error handling
|
||||
baseAPI.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
const url = error.config.baseURL + error.config.url;
|
||||
const message = `${error.message}: API request failed ${url}`;
|
||||
window.app.raise({ message });
|
||||
}
|
||||
);
|
||||
|
||||
export default baseAPI;
|
||||
14
assets/js/components/Top/types.d.ts
vendored
Normal file
14
assets/js/components/Top/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export interface Notification {
|
||||
message: string;
|
||||
time: Date;
|
||||
level: string;
|
||||
lp: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface Provider {
|
||||
title: string;
|
||||
loggedIn: boolean;
|
||||
loginPath: string;
|
||||
logoutPath: string;
|
||||
}
|
||||
|
|
@ -7,6 +7,12 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
export interface Auth {
|
||||
vehicles: VehicleLogins;
|
||||
}
|
||||
|
||||
export type VehicleLogins = Record<string, { authenticated: boolean; uri: string }>;
|
||||
|
||||
export interface FatalError {
|
||||
error: any;
|
||||
class?: any;
|
||||
|
|
@ -19,6 +25,7 @@ export interface State {
|
|||
forecast?: Forecast;
|
||||
currency?: CURRENCY;
|
||||
fatal?: FatalError;
|
||||
auth?: Auth;
|
||||
}
|
||||
|
||||
export interface LoadpointCompact {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue