Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(manager): move and update metrics cards in the server view #2241

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added server_manager/images/Material-Icons.woff2
Binary file not shown.
11 changes: 11 additions & 0 deletions server_manager/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@
http-equiv="Content-Security-Policy"
content="default-src 'self' 'unsafe-inline' outline: data:; connect-src https: 'self'; frame-src https://s3.amazonaws.com/outline-vpn/ ss:"
/>

<style>
/* We declare this here to sidestep webpack */
daniellacosse marked this conversation as resolved.
Show resolved Hide resolved
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(images/Material-Icons.woff2) format('woff2');
}
</style>

<title>Outline Manager</title>
</head>
<body>
Expand Down
26 changes: 26 additions & 0 deletions server_manager/messages/master_messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,16 @@
"message": "Access keys",
"description": "This string appears within the server view as a header of a table column that displays server access keys."
},
"server_access_keys_tab": {
daniellacosse marked this conversation as resolved.
Show resolved Hide resolved
"message": "Access keys ($KEY_COUNT$)",
"description": "This string is a tab header indicating to the user that they're currently managing their access keys.",
"placeholders": {
"KEY_COUNT": {
"content": "{keyCount}",
"example": "12"
}
}
},
"server_connections": {
"message": "Connections",
"description": "This string appears within the server view as a header of the section that displays server information and access keys."
Expand Down Expand Up @@ -1099,6 +1109,22 @@
"message": "Metrics",
"description": "This string appears within the server view as a header of the section that displays server metrics."
},
"server_metrics_data_transferred": {
daniellacosse marked this conversation as resolved.
Show resolved Hide resolved
"message": "Data transferred in the last 30 days",
"description": "This string indicates to the user that the metric displayed counts how much data was sent through the server over the last 30 days"
},
"server_metrics_user_hours": {
"message": "User hours spent on the VPN in the last 30 days",
"description": "This string indicates to the user that the metric displayed counts how many hours users used the VPN over the last 30 days."
},
"server_metrics_user_hours_unit": {
"message": "hours",
"description": "This string indicates to the user that the metric displayed is in hours."
},
"server_metrics_average_devices": {
"message": "Average number of devices used in the last 30 days",
"description": "This string indicates to the user that the metric displayed corresponds to the average number of devices that used the VPN over the last 30 days."
},
"server_my_access_key": {
"message": "My access key",
"description": "This string appears within the server view as the header for the default server access key. This key is meant to be used by the server administrator."
Expand Down
26 changes: 26 additions & 0 deletions server_manager/model/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export interface Server {
// Returns stats for bytes transferred across all access keys of this server.
getDataUsage(): Promise<BytesByAccessKey>;
daniellacosse marked this conversation as resolved.
Show resolved Hide resolved

// Returns server metrics
getServerMetrics(): Promise<ServerMetricsJson>;

// Adds a new access key to this server.
addAccessKey(): Promise<AccessKey>;

Expand Down Expand Up @@ -193,3 +196,26 @@ export type BytesByAccessKey = Map<AccessKeyId, number>;
export interface DataLimit {
readonly bytes: number;
}

export type ServerMetricsJson = {
servers: {
daniellacosse marked this conversation as resolved.
Show resolved Hide resolved
location: string;
asn: number;
asOrg: string;
tunnelTime: {
seconds: number;
};
dataTransferred: {
bytes: number;
};
}[];
accessKeys: {
accessKeyId: number;
tunnelTime: {
seconds: number;
};
dataTransferred: {
bytes: number;
};
}[];
};
35 changes: 35 additions & 0 deletions server_manager/www/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,7 @@ export class App {
this.appRoot.showError(this.appRoot.localize('error-keys-get'));
}
this.showTransferStats(server, view);
daniellacosse marked this conversation as resolved.
Show resolved Hide resolved
this.showTunnelTimeStats(server, view);
}, 0);
}

Expand Down Expand Up @@ -1074,6 +1075,23 @@ export class App {
}
}

private async refreshTunnelTimeStats(
daniellacosse marked this conversation as resolved.
Show resolved Hide resolved
selectedServer: server_model.Server,
serverView: ServerView
) {
const serverMetrics = await selectedServer.getServerMetrics();

let sum = 0;
for (const {
tunnelTime: {seconds},
} of serverMetrics.servers) {
sum += seconds / (60 * 60);
}

serverView.totalUserHours = sum;
serverView.totalDevices = serverView.totalUserHours / (30 * 24);
}

private showTransferStats(
selectedServer: server_model.Server,
serverView: ServerView
Expand All @@ -1091,6 +1109,23 @@ export class App {
}, statsRefreshRateMs);
}

private showTunnelTimeStats(
selectedServer: server_model.Server,
serverView: ServerView
) {
this.refreshTunnelTimeStats(selectedServer, serverView);
// Get transfer stats once per minute for as long as server is selected.
const statsRefreshRateMs = 60 * 1000;
const intervalId = setInterval(() => {
if (this.selectedServer !== selectedServer) {
// Server is no longer running, stop interval
clearInterval(intervalId);
return;
}
this.refreshTunnelTimeStats(selectedServer, serverView);
}, statsRefreshRateMs);
}

// Converts the access key model to the format used by outline-server-view.
private convertToUiAccessKey(
remoteAccessKey: server_model.AccessKey
Expand Down
50 changes: 50 additions & 0 deletions server_manager/www/shadowbox_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,56 @@ export class ShadowboxServer implements server.Server {
return usageMap;
}

async getServerMetrics(): Promise<server.ServerMetricsJson> {
//TODO
return {
servers: [
{
location: 'CA',
asn: 1,
asOrg: 'IDK',
tunnelTime: {
seconds: 10000,
},
dataTransferred: {
bytes: 10000,
},
},
{
location: 'US',
asn: 2,
asOrg: 'WHATEVER',
tunnelTime: {
seconds: 200000,
},
dataTransferred: {
bytes: 200000,
},
},
],
accessKeys: [
{
accessKeyId: 0,
tunnelTime: {
seconds: 10000,
},
dataTransferred: {
bytes: 10000,
},
},
{
accessKeyId: 1,
tunnelTime: {
seconds: 200000,
},
dataTransferred: {
bytes: 200000,
},
},
],
};
}

getName(): string {
return this.serverConfig?.name;
}
Expand Down
28 changes: 28 additions & 0 deletions server_manager/www/testing/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,34 @@ export class FakeServer implements server.Server {
getDataUsage() {
return Promise.resolve(new Map<server.AccessKeyId, number>());
}
getServerMetrics() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps throw an error instead. The tests should define what to return instead.

Copy link
Contributor Author

@daniellacosse daniellacosse Dec 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be clear - by extending this mock and overriding the method? Or via injection?

I see an example below!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On that note, I'd like to see some tests for the 2 scenarios (supported and unsupported), probably in app.spec.ts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"supported" here meaning with the experimental/server/metrics endpoint present or with getServerMetrics method implemented?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes basically what happens when the API doesn't return anything (it's presumably missing).

return Promise.resolve({
servers: [
{
location: '',
asn: 0,
asOrg: '',
tunnelTime: {
seconds: 0,
},
dataTransferred: {
bytes: 0,
},
},
],
accessKeys: [
{
accessKeyId: 0,
tunnelTime: {
seconds: 0,
},
dataTransferred: {
bytes: 0,
},
},
],
});
}
addAccessKey() {
const accessKey = {
id: Math.floor(Math.random()).toString(),
Expand Down
Loading
Loading