Compare commits

..
5 Commits
Author SHA1 Message Date
Maycon Santos cfea3bd489 Add ephemeral peers views and subviews (#267) 2023-09-06 13:36:23 +02:00
Zoltan Papp a44a1c5424 Feature/ephemeral peers (#263)
* Add ephemeral peer switch for SetupKeyNew component

* Add "sys" handling in activity view
2023-09-04 13:49:11 +02:00
Sarooj bukhari c2c044421f Peer search bug (#262) 2023-08-25 12:25:58 +02:00
Yulia e8d57c3445 Fix flaky Access control test (#261)
Fixed flaky test by changing the way to access 
Access Control Page and add some test ids of Add Rules buttons.

Temporary removed tests for Firefox and webkit(safari)
2023-08-22 11:38:11 +02:00
pascal-fischer 0b892c0056 Add User last login and new events
Add last login to users overview list.
Additionally added new events for dashboard login, peer expiration and peer login.
2023-08-18 19:23:56 +02:00
17 changed files with 352 additions and 135 deletions

No files matched your search

+56
View File
@@ -0,0 +1,56 @@
import { Page, test, expect } from "@playwright/test";
export class AccessControlPage {
private readonly accessControlUrl = 'http://localhost:3000/acls'
private readonly defaulAccessControl = this.page.getByRole('cell', { name: 'Default' })
private readonly deleteButton = this.page.getByRole('button', { name: 'Delete' })
private readonly deleteModal = this.page.getByTestId('confirm-delete-modal-title')
private readonly confirmButton = this.page.getByRole('button', { name: 'OK' })
private readonly addRulesButton = this.page.getByTestId('add-rule-empty-state-button')
constructor(private readonly page: Page) {}
async openAccessControlPage() {
await test.step('Open Access Control page', async () => {
await this.page.goto(this.accessControlUrl);
})
}
async assertDefaultAccessCotrolIsCreated() {
await test.step('Assert that default cotrol access is created', async () => {
await expect(this.defaulAccessControl).toBeVisible();
})
}
async pressDeleteButton() {
await test.step('Press delete button', async () => {
await this.deleteButton.click();
})
}
async assertDeleteModalIsVisibile() {
await test.step('Assert access control deletion modal is visible', async () => {
await expect(this.deleteModal).toBeVisible();
})
}
async pressConfirmButton() {
await test.step('Press confirm button on access control deletion modal', async () => {
await this.confirmButton.click();
})
}
async assertDefaultAccessCotrolIsDeleted() {
await test.step('Assert default access control should be deleted', async () => {
await expect(this.defaulAccessControl).not.toBeVisible();
})
}
async assertAddRuleButtonIsVisile() {
await test.step('Assert Add Rules button is visible', async () => {
await expect(this.addRulesButton).toBeVisible();
})
}
}
export default AccessControlPage;
+34
View File
@@ -0,0 +1,34 @@
import { Page, test, expect} from "@playwright/test";
export class LoginPage {
private readonly localUrl = 'http://localhost:3000/'
private readonly usernameField = this.page.getByPlaceholder('username@domain')
private readonly nextButton = this.page.getByRole('button', { name: 'next' })
private readonly passwordField = this.page.getByLabel('Password')
private readonly skipButton = this.page.getByRole('button', { name: 'skip' });
private readonly netBirdLogo = this.page.getByRole('link', { name: 'logo' })
constructor(private readonly page: Page) {}
async doLogin() {
await test.step('Login to local enviroment', async () => {
await this.page.goto(this.localUrl);
await this.usernameField.fill('admin@localhost');
await this.pressNextButton();
await this.passwordField.fill('testMe123@');
await this.pressNextButton();
if (await this.skipButton.isVisible({ timeout: 300 })) {
await this.skipButton.click();
}
await expect(this.netBirdLogo).toBeVisible();
})
}
async pressNextButton() {
await test.step('Press next button', async () => {
await this.nextButton.click();
})
}
}
export default LoginPage;
+1 -1
View File
@@ -1,4 +1,4 @@
import { Page, test, expect } from "@playwright/test";
import { Page, test } from "@playwright/test";
export class PeersPage {
private readonly addNewPeerButton = this.page.getByTestId('add-new-peer-button')
+15 -26
View File
@@ -1,33 +1,22 @@
import { test, expect } from '@playwright/test';
import {AddPeerModal} from '../pages/modals/add-peer-modal'
import {TopMenu} from '../pages/top-menu';
import { test } from '@playwright/test'
import {LoginPage} from '../pages/login-page'
import {AccessControlPage} from '../pages/access-control-page'
const URL = 'https://app.netbird.io/'
const localUrl = 'http://localhost:3000/'
let addPeerModal: AddPeerModal;
let topMenu: TopMenu;
let loginPage: LoginPage
let accessControlPage: AccessControlPage
test.beforeEach(async ({ page }) => {
addPeerModal = new AddPeerModal(page);
await page.goto(localUrl);
await page.getByPlaceholder('username@domain').fill('admin@localhost');
await page.getByRole('button', { name: 'next' }).click();
await page.getByLabel('Password').fill('testMe123@');
await page.getByRole('button', { name: 'next' }).click();
const skipButton = page.getByRole('button', { name: 'skip' });
if (await skipButton.isVisible({ timeout: 300 })) {
await skipButton.click();
}
await addPeerModal.assertPeerModalIsVisible();
loginPage = new LoginPage(page);
await loginPage.doLogin();
});
test('Confirm that new user has Default access', async ({ page }) => {
topMenu = new TopMenu(page);
await addPeerModal.closeAddPeerModal();
await addPeerModal.assertPeerModalIsNotVisible();
await topMenu.clickOnAccessControlOnTopMenu();
await expect(page.getByRole('cell', { name: 'Default' })).toBeVisible();
await page.getByRole('button', { name: 'Delete' }).click();
await expect(page.getByTestId('confirm-delete-modal-title')).toBeVisible();
await page.getByRole('button', { name: 'Cancel' }).click();
accessControlPage = new AccessControlPage(page);
await accessControlPage.openAccessControlPage();
await accessControlPage.assertDefaultAccessCotrolIsCreated();
await accessControlPage.pressDeleteButton();
await accessControlPage.assertDeleteModalIsVisibile();
await accessControlPage.pressConfirmButton();
await accessControlPage.assertDefaultAccessCotrolIsDeleted();
await accessControlPage.assertAddRuleButtonIsVisile();
});
+12 -19
View File
@@ -1,48 +1,41 @@
import { test } from '@playwright/test';
import { test } from '@playwright/test'
import {AddPeerModal} from '../pages/modals/add-peer-modal'
import {PeersPage} from '../pages/peers-page'
import {LoginPage} from '../pages/login-page'
const URL = 'https://app.netbird.io/'
const localUrl = 'http://localhost:3000/'
let addPeerModal: AddPeerModal;
let peersPage: PeersPage;
let addPeerModal: AddPeerModal
let peersPage: PeersPage
let loginPage: LoginPage
test.beforeEach(async ({ page }) => {
addPeerModal = new AddPeerModal(page);
await page.goto(localUrl);
await page.getByPlaceholder('username@domain').fill('admin@localhost');
await page.getByRole('button', { name: 'next' }).click();
await page.getByLabel('Password').fill('testMe123@');
await page.getByRole('button', { name: 'next' }).click();
const skipButton = page.getByRole('button', { name: 'skip' });
if (await skipButton.isVisible({ timeout: 300 })) {
await skipButton.click();
}
loginPage = new LoginPage(page);
await loginPage.doLogin();
await addPeerModal.assertPeerModalIsVisible();
});
test('Test Linux tab on a first access add peer modal / @bc', async function ({ }) {
test('Test Linux tab on a first access add peer modal / @bc', async function () {
await addPeerModal.openLinuxTab();
await addPeerModal.assertLinuxTabHasCorrectText();
});
test('Test Windows tab on a first access add peer modal / @bc', async ({ }) => {
test('Test Windows tab on a first access add peer modal / @bc', async () => {
await addPeerModal.openWindowsTab();
await addPeerModal.assertWindowsDownloadButtonHasCorrectLink();
});
test('Test MacOS tab on a first access add peer modal / @bc', async ({ }) => {
test('Test MacOS tab on a first access add peer modal / @bc', async () => {
await addPeerModal.openMacTab();
await addPeerModal.assertIntelDownloadButtonHasCorrectLink();
await addPeerModal.assertM1M2DownloadButtonHasCorrectLink();
});
test('Test Android tab on a first access add peer modal', async ({ }) => {
test('Test Android tab on a first access add peer modal', async () => {
await addPeerModal.openAndroidTab();
await addPeerModal.assertAndroidDownloadButtonHasCorrectLink();
});
test('Test Docker tab on a first access add peer modal', async ({ }) => {
test('Test Docker tab on a first access add peer modal', async () => {
await addPeerModal.openDockerTab();
await addPeerModal.assertDockerDownloadButtonHasCorrectLink();
});
+9 -9
View File
@@ -39,15 +39,15 @@ export default defineConfig({
use: { channel: 'chrome', },
},
{
name: 'firefox',
use: { browserName: 'firefox', },
},
{
name: 'webkit',
use: { browserName: 'webkit', },
},
// {
// name: 'firefox',
// use: { browserName: 'firefox', },
// },
//
// {
// name: 'webkit',
// use: { browserName: 'webkit', },
// },
/* Test against mobile viewports. */
// {
+80 -39
View File
@@ -117,6 +117,7 @@ const SetupKeyNew = (props: any) => {
groupsToCreate: groupsToCreate,
expires_in: expiresIn,
usage_limit: formSetupKey.usage_limit,
ephemeral: formSetupKey.ephemeral,
} as SetupKeyToSave;
};
@@ -378,50 +379,41 @@ const SetupKeyNew = (props: any) => {
)}
<Row style={{ marginTop: `${isGroupUpdateView ? "0" : "39px"}` }}>
<Col
xs={24}
sm={24}
md={!isGroupUpdateView ? 11 : 24}
lg={!isGroupUpdateView ? 11 : 24}
xl={!isGroupUpdateView ? 11 : 24}
xxl={!isGroupUpdateView ? 11 : 24}
span={!isGroupUpdateView ? 11 : 24}
style={{
paddingRight: `${!isGroupUpdateView ? "70px" : "0"}`,
}}
xs={24}
sm={24}
md={11}
lg={11}
xl={11}
xxl={11}
span={11}
style={{
paddingRight: "70px",
}}
>
<Paragraph
style={{
whiteSpace: "pre-line",
margin: 0,
fontWeight: "500",
}}
style={{
whiteSpace: "pre-line",
margin: 0,
fontWeight: "500",
}}
>
Auto-assigned groups
<Paragraph
style={{
whiteSpace: "pre-line",
margin: 0,
fontWeight: "500",
}}
></Paragraph>
Ephemeral peers enabled?
</Paragraph>
<Col>
<Input
disabled
value={formSetupKey.ephemeral? "yes" : "no"}
suffix={<LockOutlined style={{ color: "#BFBFBF" }} />}
style={{ marginTop: "8px" }}
/>
<Col span={24}>
<Form.Item
style={{ marginTop: "8px", marginBottom: 0 }}
name="autoGroupNames"
rules={[{ validator: selectValidator }]}
>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Associate groups with the key"
tagRender={blueTagRender}
dropdownRender={dropDownRender}
// enabled only when we have a new key !setupkey.id or when the key is valid
disabled={!(!setupKey.id || setupKey.valid)}
optionFilterProp="serchValue"
>
{tagGroups.map((m, index) => (
<Option key={index} value={m.id} serchValue={m.name}>
{optionRender(m.name, m.id)}
</Option>
))}
</Select>
</Form.Item>
</Col>
</Col>
{!isGroupUpdateView && (
@@ -442,6 +434,55 @@ const SetupKeyNew = (props: any) => {
</Col>
)}
</Row>
<Row style={{ marginTop: `${isGroupUpdateView ? "0" : "39px"}` }}>
<Col
xs={24}
sm={24}
md={!isGroupUpdateView ? 11 : 24}
lg={!isGroupUpdateView ? 11 : 24}
xl={!isGroupUpdateView ? 11 : 24}
xxl={!isGroupUpdateView ? 11 : 24}
span={!isGroupUpdateView ? 11 : 24}
style={{
paddingRight: `${!isGroupUpdateView ? "70px" : "0"}`,
}}
>
<Paragraph
style={{
whiteSpace: "pre-line",
margin: 0,
fontWeight: "500",
}}
>
Auto-assigned groups
</Paragraph>
<Col span={24}>
<Form.Item
style={{ marginTop: "8px", marginBottom: 0 }}
name="autoGroupNames"
rules={[{ validator: selectValidator }]}
>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Associate groups with the key"
tagRender={blueTagRender}
dropdownRender={dropDownRender}
// enabled only when we have a new key !setupkey.id or when the key is valid
disabled={!(!setupKey.id || setupKey.valid)}
optionFilterProp="searchValue"
>
{tagGroups.map((m, index) => (
<Option key={index} value={m.id} serchValue={m.name}>
{optionRender(m.name, m.id)}
</Option>
))}
</Select>
</Form.Item>
</Col>
</Col>
</Row>
</Form>
</div>
<Container
+38
View File
@@ -75,6 +75,7 @@ const SetupKeyNew = () => {
const expiresIn = formSetupKey.expires_in
? formSetupKey.expires_in * 24 * 3600
: 7 * 24 * 3600; // the api expects seconds we have days
return {
id: formSetupKey.id,
name: formSetupKey.name,
@@ -84,6 +85,7 @@ const SetupKeyNew = () => {
groupsToCreate: groupsToCreate,
expires_in: expiresIn,
usage_limit: formSetupKey.usage_limit,
ephemeral: formSetupKey.ephemeral,
} as SetupKeyToSave;
};
@@ -155,6 +157,7 @@ const SetupKeyNew = () => {
usage_limit: 0,
used_times: 0,
expires_in: 7,
ephemeral: false,
} as SetupKey)
);
setFormSetupKey({} as FormSetupKey);
@@ -487,6 +490,41 @@ const SetupKeyNew = () => {
</Col>
</Row>
<Row style={{ marginTop: "24px" }} justify={"space-between"}>
<Col span={18}>
<Paragraph
style={{
whiteSpace: "pre-line",
margin: 0,
fontWeight: "500",
}}
>
Ephemeral peers
</Paragraph>
<Paragraph
type={"secondary"}
style={{ whiteSpace: "pre-line", margin: 0 }}
>
Use this type of key to add peers that will be auto removed after being inactive
</Paragraph>
</Col>
<Col span={6}>
<Row justify={"end"}>
<Form.Item name="ephemeral" valuePropName="checked">
<Switch
size="small"
onChange={(checked) => {
setFormSetupKey({
...formSetupKey,
ephemeral: checked,
});
}}
/>
</Form.Item>
</Row>
</Col>
</Row>
<Row style={{ marginTop: "24px" }}>
<Col span={24}>
<Paragraph
+2 -1
View File
@@ -63,7 +63,8 @@ export function* saveSetupKey(action: ReturnType<typeof actions.saveSetupKey.req
auto_groups: newGroups,
type: keyToSave.type,
expires_in: keyToSave.expires_in,
usage_limit: keyToSave.usage_limit
usage_limit: keyToSave.usage_limit,
ephemeral: keyToSave.ephemeral
} as SetupKeyToSave
});
} else {
+1
View File
@@ -14,6 +14,7 @@ export interface SetupKey {
auto_groups: string[]
expires_in: number;
usage_limit: any;
ephemeral: boolean;
}
export interface FormSetupKey extends SetupKey {
+1
View File
@@ -8,6 +8,7 @@ export interface User {
is_current?: boolean;
is_service_user?: boolean;
is_blocked?: boolean;
last_login?: string;
}
export interface FormUser extends User {
+3 -1
View File
@@ -100,7 +100,7 @@ export const timeAgo = (dateParam) => {
const isToday = today.toDateString() === date.toDateString();
const isYesterday = yesterday.toDateString() === date.toDateString();
const isThisYear = today.getFullYear() === date.getFullYear();
const never = date.getFullYear() === 1;
if (seconds < -1) {
return getFormattedDate(date, false, true);
@@ -118,6 +118,8 @@ export const timeAgo = (dateParam) => {
return getFormattedDate(date, 'yesterday'); // Yesterday at 10:20
} else if (isThisYear) {
return getFormattedDate(date, false, true); // 10. January at 10:20
} else if (never) {
return 'never';
}
return getFormattedDate(date); // 10. January 2017. at 10:20
+2
View File
@@ -666,6 +666,7 @@ export const AccessControl = () => {
<Row justify="end">
<Col>
<Button
data-testid="add-rule-main-button"
type="primary"
disabled={savedPolicy.loading}
onClick={onClickAddNewPolicy}
@@ -719,6 +720,7 @@ export const AccessControl = () => {
</a>
</Paragraph>
<Button
data-testid="add-rule-empty-state-button"
size={"middle"}
type="primary"
onClick={() => onClickAddNewPolicy()}
+33
View File
@@ -169,7 +169,37 @@ export const Activity = () => {
const renderInitiator = (event: EventDataTable) => {
let body = <></>;
const user = users?.find((u) => u.id === event.initiator_id);
if (event.initiator_id == "sys") {
body = (
<span
style={{
height: "auto",
whiteSpace: "normal",
textAlign: "left",
}}
>
<Row>
<Text type="secondary">System</Text>
</Row>
</span>
);
}
switch (event.activity_code) {
case "peer.login.expire":
body = (
<span
style={{
height: "auto",
whiteSpace: "normal",
textAlign: "left",
}}
>
<Row>
<Text type="secondary">System</Text>
</Row>
</span>
);
break;
case "setupkey.peer.add":
const key = setupKeys?.find((k) => k.id === event.initiator_id);
if (key) {
@@ -255,6 +285,7 @@ export const Activity = () => {
switch (event.activity_code) {
case "account.create":
case "user.join":
case "dashboard.login":
return "-";
case "rule.add":
case "rule.delete":
@@ -290,6 +321,8 @@ export const Activity = () => {
case "peer.rename":
case "peer.login.expiration.disable":
case "peer.login.expiration.enable":
case "user.peer.login":
case "peer.login.expire":
return renderMultiRowSpan(event.meta.fqdn, event.meta.ip);
case "route.add":
case "route.delete":
+3 -1
View File
@@ -248,6 +248,7 @@ export const Peers = () => {
const t = searchText
? searchText.toLowerCase().trim()
: textToSearch.toLowerCase().trim();
let f: Peer[] = filter(peers, (f: Peer) => {
let userEmail: string | null;
const u = users?.find((u) => u.id === f.user_id)?.email;
@@ -261,8 +262,9 @@ export const Peers = () => {
(userEmail && userEmail.toLowerCase().includes(t))
);
}) as Peer[];
if (optionOnOff === "on") {
f = filter(peers, (f: Peer) => f.connected);
f = filter(f, (f: Peer) => f.connected);
}
let filterByGroupArray: any = [];
+35 -26
View File
@@ -31,7 +31,7 @@ import { useGetTokenSilently } from "../utils/token";
import { actions as groupActions } from "../store/group";
import { Group } from "../store/group/types";
import { TooltipPlacement } from "antd/es/tooltip";
import { capitalize, isLocalDev, isNetBirdHosted } from "../utils/common";
import {capitalize, isLocalDev, isNetBirdHosted, timeAgo} from "../utils/common";
import { usePageSizeHelpers } from "../utils/pageSize";
import AddServiceUserPopup from "../components/popups/AddServiceUserPopup";
import InviteUserPopup from "../components/popups/InviteUserPopup";
@@ -499,45 +499,54 @@ export const RegularUsers = () => {
defaultSortOrder="ascend"
render={(text, record, index) => {
const btn = (
<Button
type="text"
onClick={() =>
handleEditUser(record as UserDataTable)
}
className="tooltip-label"
>
<Text className="font-500">
{text && text.trim() !== ""
? text
: (record as User).id}
</Text>
</Button>
<Button
type="text"
onClick={() =>
handleEditUser(record as UserDataTable)
}
className="tooltip-label"
>
<Text className="font-500">
{text && text.trim() !== ""
? text
: (record as User).id}
</Text>
</Button>
);
if ((record as User).is_current) {
return (
<div>
{btn}
<Tag color="blue">me</Tag>
</div>
<>
<div>
{btn}
<Tag color="blue">me</Tag>
</div>
<Text type={"secondary"} style={{paddingLeft: "15px"}}>Last login: {String(timeAgo((record as User).last_login))}</Text>
</>
);
}
if ((record as User).status === "invited") {
return (
<div>
{btn}
<Tag color="gold">invited</Tag>
</div>
<>
<div>
{btn}
<Tag color="gold">invited</Tag>
</div>
<Text type={"secondary"} style={{paddingLeft: "15px", paddingTop: "0px"}}>Last login: {String(timeAgo((record as User).last_login))}</Text>
</>
);
}
if ((record as User).status === "blocked") {
return (
<div>
{btn}
<Tag color="red">blocked</Tag>
</div>
<>
<div>
{btn}
<Tag color="red">blocked</Tag>
</div>
<Text type={"secondary"} style={{paddingLeft: "15px"}}>Last login: {String(timeAgo((record as User).last_login))}</Text>
</>
);
}
+27 -12
View File
@@ -576,6 +576,14 @@ export const SetupKeys = () => {
(a as any).name.localeCompare((b as any).name)
}
render={(text, record: any, index) => {
let sk = record as SetupKeyDataTable;
let expiry = sk.ephemeral ? (
<Tag>
<Text type="secondary" style={{ fontSize: 10 }}>
ephemeral peers enabled
</Text>
</Tag>
) : null;
return (
<Button
type="text"
@@ -584,18 +592,25 @@ export const SetupKeys = () => {
}
className="tooltip-label"
>
{" "}
<Text className="font-500">
<Badge
size={"small"}
status={
record.state === "valid"
? "success"
: "error"
}
text={text}
></Badge>
</Text>
<span style={{ textAlign: "left" }}>
<Row>
{" "}
<Text className="font-500">
<Badge
size={"small"}
status={
record.state === "valid"
? "success"
: "error"
}
text={text}
></Badge>
</Text>
</Row>
<Row >
{expiry}
</Row>
</span>
</Button>
);
}}