1import { isEmpty, isString, set } from 'lodash';
2import { createSlice, PayloadAction } from '@reduxjs/toolkit';
3import { dateTimeFormat, dateTimeFormatTimeAgo, setWeekStart, TimeZone } from '@grafana/data';
4
5import { Team, ThunkResult, UserDTO, UserOrg, UserSession } from 'app/types';
6import config from 'app/core/config';
7import { contextSrv } from 'app/core/core';
8
9export interface UserState {
10  orgId: number;
11  timeZone: TimeZone;
12  weekStart: string;
13  fiscalYearStartMonth: number;
14  user: UserDTO | null;
15  teams: Team[];
16  orgs: UserOrg[];
17  sessions: UserSession[];
18  teamsAreLoading: boolean;
19  orgsAreLoading: boolean;
20  sessionsAreLoading: boolean;
21  isUpdating: boolean;
22}
23
24export const initialUserState: UserState = {
25  orgId: config.bootData.user.orgId,
26  timeZone: config.bootData.user.timezone,
27  weekStart: config.bootData.user.weekStart,
28  fiscalYearStartMonth: 0,
29  orgsAreLoading: false,
30  sessionsAreLoading: false,
31  teamsAreLoading: false,
32  isUpdating: false,
33  orgs: [],
34  sessions: [],
35  teams: [],
36  user: null,
37};
38
39export const slice = createSlice({
40  name: 'user/profile',
41  initialState: initialUserState,
42  reducers: {
43    updateTimeZone: (state, action: PayloadAction<{ timeZone: TimeZone }>) => {
44      state.timeZone = action.payload.timeZone;
45    },
46    updateWeekStart: (state, action: PayloadAction<{ weekStart: string }>) => {
47      state.weekStart = action.payload.weekStart;
48    },
49    updateFiscalYearStartMonth: (state, action: PayloadAction<{ fiscalYearStartMonth: number }>) => {
50      state.fiscalYearStartMonth = action.payload.fiscalYearStartMonth;
51    },
52    setUpdating: (state, action: PayloadAction<{ updating: boolean }>) => {
53      state.isUpdating = action.payload.updating;
54    },
55    userLoaded: (state, action: PayloadAction<{ user: UserDTO }>) => {
56      state.user = action.payload.user;
57    },
58    initLoadTeams: (state, action: PayloadAction<undefined>) => {
59      state.teamsAreLoading = true;
60    },
61    teamsLoaded: (state, action: PayloadAction<{ teams: Team[] }>) => {
62      state.teams = action.payload.teams;
63      state.teamsAreLoading = false;
64    },
65    initLoadOrgs: (state, action: PayloadAction<undefined>) => {
66      state.orgsAreLoading = true;
67    },
68    orgsLoaded: (state, action: PayloadAction<{ orgs: UserOrg[] }>) => {
69      state.orgs = action.payload.orgs;
70      state.orgsAreLoading = false;
71    },
72    initLoadSessions: (state, action: PayloadAction<undefined>) => {
73      state.sessionsAreLoading = true;
74    },
75    sessionsLoaded: (state, action: PayloadAction<{ sessions: UserSession[] }>) => {
76      const sorted = action.payload.sessions.sort((a, b) => Number(b.isActive) - Number(a.isActive)); // Show active sessions first
77      state.sessions = sorted.map((session) => ({
78        id: session.id,
79        isActive: session.isActive,
80        seenAt: dateTimeFormatTimeAgo(session.seenAt),
81        createdAt: dateTimeFormat(session.createdAt, { format: 'MMMM DD, YYYY' }),
82        clientIp: session.clientIp,
83        browser: session.browser,
84        browserVersion: session.browserVersion,
85        os: session.os,
86        osVersion: session.osVersion,
87        device: session.device,
88      }));
89      state.sessionsAreLoading = false;
90    },
91    userSessionRevoked: (state, action: PayloadAction<{ tokenId: number }>) => {
92      state.sessions = state.sessions.filter((session: UserSession) => {
93        return session.id !== action.payload.tokenId;
94      });
95      state.isUpdating = false;
96    },
97  },
98});
99
100export const updateFiscalYearStartMonthForSession = (fiscalYearStartMonth: number): ThunkResult<void> => {
101  return async (dispatch) => {
102    set(contextSrv, 'user.fiscalYearStartMonth', fiscalYearStartMonth);
103    dispatch(updateFiscalYearStartMonth({ fiscalYearStartMonth }));
104  };
105};
106
107export const updateTimeZoneForSession = (timeZone: TimeZone): ThunkResult<void> => {
108  return async (dispatch) => {
109    if (!isString(timeZone) || isEmpty(timeZone)) {
110      timeZone = config?.bootData?.user?.timezone;
111    }
112
113    set(contextSrv, 'user.timezone', timeZone);
114    dispatch(updateTimeZone({ timeZone }));
115  };
116};
117
118export const updateWeekStartForSession = (weekStart: string): ThunkResult<void> => {
119  return async (dispatch) => {
120    if (!isString(weekStart) || isEmpty(weekStart)) {
121      weekStart = config?.bootData?.user?.weekStart;
122    }
123
124    set(contextSrv, 'user.weekStart', weekStart);
125    dispatch(updateWeekStart({ weekStart }));
126    setWeekStart(weekStart);
127  };
128};
129
130export const {
131  setUpdating,
132  initLoadOrgs,
133  orgsLoaded,
134  initLoadTeams,
135  teamsLoaded,
136  userLoaded,
137  userSessionRevoked,
138  initLoadSessions,
139  sessionsLoaded,
140  updateTimeZone,
141  updateWeekStart,
142  updateFiscalYearStartMonth,
143} = slice.actions;
144
145export const userReducer = slice.reducer;
146export default { user: slice.reducer };
147