Skip to content

Commit

Permalink
Add filtering by team, is_currently_oncall and search on the user page (
Browse files Browse the repository at this point in the history
#4575)

# What this PR does

This PR adds filtering by team and is_currently_oncall on the user page

## Which issue(s) this PR closes

Closes #4353

<!--
*Note*: if you have more than one GitHub issue that this PR closes, be
sure to preface
each issue link with a [closing
keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue).
This ensures that the issue(s) are auto-closed once the PR has been
merged.
-->

## Checklist

- [ ] Unit, integration, and e2e (if applicable) tests updated
- [ ] Documentation added (or `pr:no public docs` PR label added if not
required)
- [ ] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
    show up in the autogenerated release notes.
  • Loading branch information
iskhakov authored Jul 8, 2024
1 parent 0261272 commit 24aa3a5
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 59 deletions.
2 changes: 1 addition & 1 deletion Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ docker_build_sub(
"localhost:63628/oncall/engine:dev",
context="./engine",
cache_from=["grafana/oncall:latest", "grafana/oncall:dev"],
ignore=["./test-results/", "./grafana-plugin/dist/", "./grafana-plugin/e2e-tests/"],
ignore=["./test-results/", "./grafana-plugin/dist/", "./grafana-plugin/e2e-tests/", "./grafana-plugin/node_modules/"],
child_context=".",
target="dev",
extra_cmds=["ADD ./grafana-plugin/src/plugin.json /etc/grafana-plugin/src/plugin.json"],
Expand Down
41 changes: 41 additions & 0 deletions engine/apps/api/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ class UserView(
"send_test_sms": [RBACPermission.Permissions.USER_SETTINGS_WRITE],
"export_token": [RBACPermission.Permissions.USER_SETTINGS_WRITE],
"upcoming_shifts": [RBACPermission.Permissions.USER_SETTINGS_READ],
"filters": [RBACPermission.Permissions.USER_SETTINGS_READ],
}

rbac_object_permissions = {
Expand Down Expand Up @@ -846,6 +847,46 @@ def export_token(self, request, pk) -> Response:
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)

@extend_schema(
responses=inline_serializer(
name="UserFilters",
fields={
"name": serializers.CharField(),
"type": serializers.CharField(),
"href": serializers.CharField(required=False),
"global": serializers.BooleanField(required=False),
"default": serializers.JSONField(required=False),
"description": serializers.CharField(required=False),
"options": inline_serializer(
name="UserFiltersOptions",
fields={
"value": serializers.CharField(),
"display_name": serializers.IntegerField(),
},
),
},
many=True,
)
)
@action(methods=["get"], detail=False)
def filters(self, request):
filter_name = request.query_params.get("search", None)
api_root = "/api/internal/v1/"

filter_options = [
{
"name": "team",
"type": "team_select",
"href": api_root + "teams/",
"global": True,
},
]

if filter_name is not None:
filter_options = list(filter(lambda f: filter_name in f["name"], filter_options))

return Response(filter_options)


def handle_phone_notificator_failed(exc: BaseFailed) -> Response:
if exc.graceful_msg:
Expand Down
10 changes: 7 additions & 3 deletions grafana-plugin/e2e-tests/users/usersActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,13 @@ test.describe('Users screen actions', () => {

await page.waitForTimeout(2000);

const searchInput = page.locator(`[data-testid="search-users"]`);

await searchInput.fill(userName);
await page
.locator('div')
.filter({ hasText: /^Search or filter results\.\.\.$/ })
.nth(1)
.click();
await page.keyboard.insertText(userName);
await page.keyboard.press('Enter');
await page.waitForTimeout(2000);

const result = page.locator(`[data-testid="users-username"]`);
Expand Down
5 changes: 2 additions & 3 deletions grafana-plugin/src/models/user/user.helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,8 @@ export class UserHelper {
* NOTE: if is_currently_oncall=all the backend will not paginate the results, it will send back an array of ALL users
*/
static async search(f: any = { searchTerm: '' }, page = 1) {
const filters = typeof f === 'string' ? { searchTerm: f } : f; // for GSelect compatibility
const { searchTerm: search, ...restFilters } = filters;
return (await onCallApi().GET('/users/', { params: { query: { search, page, ...restFilters } } })).data;
const filters = typeof f === 'string' ? { search: f } : f; // for GSelect compatibility
return (await onCallApi().GET('/users/', { params: { query: { ...filters, page } } })).data;
}

static getSearchResult(userStore: UserStore) {
Expand Down
7 changes: 6 additions & 1 deletion grafana-plugin/src/models/user/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import dayjs from 'dayjs';
import { get } from 'lodash-es';
import { action, computed, runInAction, makeAutoObservable } from 'mobx';

import { RemoteFiltersType } from 'containers/RemoteFilters/RemoteFilters.types';
import { ActionKey } from 'models/loader/action-keys';
import { NotificationPolicyType } from 'models/notification_policy/notification_policy';
import { makeRequest } from 'network/network';
Expand Down Expand Up @@ -36,7 +37,11 @@ export class UserStore {
this.rootStore = rootStore;
}

async fetchItems(f: any = { searchTerm: '' }, page = 1, invalidateFn?: () => boolean): Promise<any> {
async fetchItems(
f: RemoteFiltersType | string = { searchTerm: '', type: undefined, used: undefined },
page = 1,
invalidateFn?: () => boolean
): Promise<any> {
const response = await UserHelper.search(f, page);

if (invalidateFn && invalidateFn()) {
Expand Down
4 changes: 4 additions & 0 deletions grafana-plugin/src/pages/users/Users.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { GrafanaTheme2 } from '@grafana/data';

export const getUsersStyles = (theme: GrafanaTheme2) => {
return {
filters: css`
margin-bottom: 20px;
`,

usersTtitle: css`
display: flex;
align-items: center;
Expand Down
87 changes: 36 additions & 51 deletions grafana-plugin/src/pages/users/Users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {
import { PluginLink } from 'components/PluginLink/PluginLink';
import { Text } from 'components/Text/Text';
import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge';
import { UsersFilters } from 'components/UsersFilters/UsersFilters';
import { RemoteFilters } from 'containers/RemoteFilters/RemoteFilters';
import { RemoteFiltersType } from 'containers/RemoteFilters/RemoteFilters.types';
import { UserSettings } from 'containers/UserSettings/UserSettings';
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
import { UserHelper } from 'models/user/user.helpers';
Expand All @@ -44,9 +45,8 @@ const REQUIRED_PERMISSION_TO_VIEW_USERS = UserActions.UserSettingsWrite;
interface UsersState extends PageBaseState {
isWrongTeam: boolean;
userPkToEdit?: ApiSchemas['User']['pk'] | 'new';
usersFilters?: {
searchTerm: string;
};

filters: RemoteFiltersType;
}

@observer
Expand All @@ -62,9 +62,7 @@ class Users extends React.Component<UsersProps, UsersState> {
this.state = {
isWrongTeam: false,
userPkToEdit: undefined,
usersFilters: {
searchTerm: '',
},
filters: { searchTerm: '', type: undefined, used: undefined, mine: undefined },

errorData: initErrorDataState(),
};
Expand All @@ -80,7 +78,7 @@ class Users extends React.Component<UsersProps, UsersState> {

updateUsers = debounce(async (invalidateFn?: () => boolean) => {
const { store } = this.props;
const { usersFilters } = this.state;
const { filters } = this.state;
const { userStore, filtersStore } = store;
const page = filtersStore.currentTablePageNum[PAGE.Users];

Expand All @@ -89,7 +87,7 @@ class Users extends React.Component<UsersProps, UsersState> {
}

LocationHelper.update({ p: page }, 'partial');
await userStore.fetchItems(usersFilters, page, invalidateFn);
await userStore.fetchItems(filters, page, invalidateFn);

this.forceUpdate();
}, DEBOUNCE_MS);
Expand Down Expand Up @@ -184,38 +182,20 @@ class Users extends React.Component<UsersProps, UsersState> {
renderContentIfAuthorized(authorizedToViewUsers: boolean) {
const {
store: { userStore, filtersStore },
theme,
} = this.props;

const { usersFilters, userPkToEdit } = this.state;
const { userPkToEdit } = this.state;

const page = filtersStore.currentTablePageNum[PAGE.Users];

const { count, results, page_size } = UserHelper.getSearchResult(userStore);
const columns = this.getTableColumns();

const handleClear = () =>
this.setState({ usersFilters: { searchTerm: '' } }, () => {
this.updateUsers();
});
const styles = getUsersStyles(theme);

return (
<>
{authorizedToViewUsers ? (
<>
<div className={styles.userFiltersContainer} data-testid="users-filters">
<UsersFilters
className={styles.usersFilters}
value={usersFilters}
isLoading={results === undefined}
onChange={this.handleUsersFiltersChange}
/>
<Button variant="secondary" icon="times" onClick={handleClear}>
Clear filters
</Button>
</div>

{this.renderFilters()}
<GTable
data-testid="users-table"
emptyText={results ? 'No users found' : 'Loading...'}
Expand Down Expand Up @@ -250,6 +230,33 @@ class Users extends React.Component<UsersProps, UsersState> {
);
}

renderFilters() {
const { query, store, theme } = this.props;
const styles = getUsersStyles(theme);

return (
<div className={styles.filters}>
<RemoteFilters
query={query}
page={PAGE.Users}
grafanaTeamStore={store.grafanaTeamStore}
onChange={this.handleFiltersChange}
/>
</div>
);
}

handleFiltersChange = (filters: RemoteFiltersType, _isOnMount: boolean) => {
const { filtersStore } = this.props.store;
const currentTablePage = filtersStore.currentTablePageNum[PAGE.Users];

LocationHelper.update({ p: currentTablePage }, 'partial');

this.setState({ filters }, () => {
this.updateUsers();
});
};

renderTitle = (user: ApiSchemas['User']) => {
const {
store: { userStore },
Expand Down Expand Up @@ -288,18 +295,6 @@ class Users extends React.Component<UsersProps, UsersState> {
return user.notification_chain_verbal.important;
};

renderContacts = (user: ApiSchemas['User']) => {
const { store } = this.props;
return (
<div>
<div>Slack: {user.slack_user_identity?.name || '-'}</div>
{store.hasFeature(AppFeature.Telegram) && (
<div>Telegram: {user.telegram_configuration?.telegram_nick_name || '-'}</div>
)}
</div>
);
};

renderButtons = (user: ApiSchemas['User']) => {
const { store } = this.props;
const { userStore } = store;
Expand Down Expand Up @@ -442,16 +437,6 @@ class Users extends React.Component<UsersProps, UsersState> {
this.updateUsers();
};

handleUsersFiltersChange = (usersFilters: any, invalidateFn: () => boolean) => {
const { filtersStore } = this.props.store;

filtersStore.currentTablePageNum[PAGE.Users] = 1;

this.setState({ usersFilters }, () => {
this.updateUsers(invalidateFn);
});
};

handleHideUserSettings = () => {
const { history } = this.props;
this.setState({ userPkToEdit: undefined });
Expand Down

0 comments on commit 24aa3a5

Please sign in to comment.