fix(ui): some issue/assignment refactoring

This commit is contained in:
sylenien 2023-01-02 17:59:16 +01:00
parent df12385e5f
commit f6a62d835a
17 changed files with 188 additions and 179 deletions

View file

@ -36,7 +36,7 @@ function SessionList(props: Props) {
</div>
<Loader loading={props.loading}>
<NoContent
show={!props.loading && props.list.size === 0}
show={!props.loading && props.list.length === 0}
title={
<div className="flex items-center justify-center flex-col">
<AnimatedSVG name={ICONS.NO_LIVE_SESSIONS} size={170} />

View file

@ -53,5 +53,5 @@ export default connect(state => ({
users: state.getIn(['assignments', 'users']),
loading: state.getIn(['assignments', 'fetchAssignment', 'loading']),
issueTypeIcons: state.getIn(['assignments', 'issueTypeIcons']),
issuesIntegration: state.getIn([ 'issues', 'list']).first() || {},
issuesIntegration: state.getIn([ 'issues', 'list'])[0] || {},
}))(IssueDetails);

View file

@ -1,7 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { Form, Input, Button, CircularLoader, Loader } from 'UI';
//import { } from 'Duck/issues';
import { addActivity, init, edit, fetchAssignments, fetchMeta } from 'Duck/assignments';
import Select from 'Shared/Select';
@ -119,7 +118,6 @@ class IssueForm extends React.PureComponent {
selection
name="assignee"
options={userOptions}
// value={ instance.assignee }
fluid
onChange={this.writeOption}
placeholder="Select a user"
@ -153,7 +151,7 @@ class IssueForm extends React.PureComponent {
<Button
loading={creating}
variant="primary"
disabled={!instance.validate()}
disabled={!instance.isValid}
className="float-left mr-2"
type="submit"
>

View file

@ -1,9 +1,8 @@
import React from 'react';
import { connect } from 'react-redux';
import { Icon, Popover, Button } from 'UI';
import { Popover, Button } from 'UI';
import IssuesModal from './IssuesModal';
import { fetchProjects, fetchMeta } from 'Duck/assignments';
import stl from './issues.module.css';
@connect(
(state) => ({
@ -15,9 +14,7 @@ import stl from './issues.module.css';
fetchIssueLoading: state.getIn(['assignments', 'fetchAssignment', 'loading']),
fetchIssuesLoading: state.getIn(['assignments', 'fetchAssignments', 'loading']),
projectsLoading: state.getIn(['assignments', 'fetchProjects', 'loading']),
issuesIntegration: state.getIn(['issues', 'list']).first() || {},
jiraConfig: state.getIn(['issues', 'list']).first(),
issuesIntegration: state.getIn(['issues', 'list']) || {},
issuesFetched: state.getIn(['issues', 'issuesFetched']),
}),
{ fetchMeta, fetchProjects }
@ -58,13 +55,9 @@ class Issues extends React.Component {
render() {
const {
sessionId,
isModalDisplayed,
projectsLoading,
metaLoading,
fetchIssuesLoading,
issuesIntegration,
} = this.props;
const provider = issuesIntegration.provider;
const provider = issuesIntegration.first()?.provider || '';
return (
<Popover
@ -80,13 +73,6 @@ class Issues extends React.Component {
Create Issue
</Button>
</div>
{/* <div
className="flex items-center cursor-pointer"
disabled={!isModalDisplayed && (metaLoading || fetchIssuesLoading || projectsLoading)}
>
<Icon name={`integrations/${provider === 'jira' ? 'jira' : 'github'}`} size="16" />
<span className="ml-2 whitespace-nowrap">Create Issue</span>
</div> */}
</Popover>
);
}

View file

@ -10,7 +10,7 @@ import styles from './playerBlock.module.css';
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
sessionId: state.getIn(['sessions', 'current']).sessionId,
disabled: state.getIn(['components', 'targetDefiner', 'inspectorMode']),
jiraConfig: state.getIn(['issues', 'list']).first(),
jiraConfig: state.getIn(['issues', 'list'])[0],
}))
export default class PlayerBlock extends React.PureComponent {
render() {

View file

@ -169,7 +169,7 @@ function SessionList(props: Props) {
</Button>
</div>
}
show={!loading && list.size === 0}
show={!loading && list.length === 0}
>
{list.map((session: any) => (
<div key={session.sessionId} className="border-b">
@ -188,7 +188,7 @@ function SessionList(props: Props) {
<div className="flex items-center justify-between p-5">
<div>
Showing <span className="font-medium">{(currentPage - 1) * pageSize + 1}</span> to{' '}
<span className="font-medium">{(currentPage - 1) * pageSize + list.size}</span> of{' '}
<span className="font-medium">{(currentPage - 1) * pageSize + list.length}</span> of{' '}
<span className="font-medium">{numberWithCommas(total)}</span> sessions.
</div>
<Pagination

View file

@ -2,10 +2,9 @@ import { List, Map, Set } from 'immutable';
import Assignment from 'Types/session/assignment';
import Activity from 'Types/session/activity';
import withRequestState, { RequestTypes } from './requestStateCreator';
import { createListUpdater, createItemInListUpdater } from './funcTools/tools';
import { createListUpdater } from './funcTools/tools';
import { editType, initType } from './funcTools/crud/types';
import { createInit, createEdit } from './funcTools/crud';
import IssuesType from 'Types/issue/issuesType'
const idKey = 'id';
const name = 'assignment';
@ -22,8 +21,8 @@ const INIT = initType(name);
const initialState = Map({
list: List(),
instance: Assignment(),
activeIssue: Assignment(),
instance: new Assignment(),
activeIssue: new Assignment(),
issueTypes: List(),
issueTypeIcons: Set(),
users: List(),
@ -33,22 +32,23 @@ const initialState = Map({
const reducer = (state = initialState, action = {}) => {
const users = state.get('users');
var issueTypes = []
let issueTypes = []
switch (action.type) {
case INIT:
action.instance.issueType = issueTypes.length > 0 ? issueTypes[0].id : '';
return state.set('instance', Assignment(action.instance));
return state.set('instance', new Assignment(action.instance));
case EDIT:
return state.mergeIn([ 'instance' ], action.instance);
const inst = state.get('instance')
return state.set('instance', new Assignment({ ...inst, ...action.instance }));
case FETCH_PROJECTS.SUCCESS:
return state.set('projects', List(action.data)).set('projectsFetched', true);
case FETCH_ASSIGNMENTS.SUCCESS:
return state.set('list', List(action.data).map(Assignment));
return state.set('list', List(action.data).map(as => new Assignment(as)));
case FETCH_ASSIGNMENT.SUCCESS:
return state.set('activeIssue', Assignment({ ...action.data, users}));
return state.set('activeIssue', new Assignment({ ...action.data, users}));
case FETCH_META.SUCCESS:
issueTypes = action.data.issueTypes
var issueTypeIcons = {}
const issueTypeIcons = {}
issueTypes.forEach(iss => {
issueTypeIcons[iss.id] = iss.iconUrl
})
@ -56,12 +56,12 @@ const reducer = (state = initialState, action = {}) => {
.set('users', List(action.data.users))
.set('issueTypeIcons', issueTypeIcons)
case ADD_ACTIVITY.SUCCESS:
const instance = Assignment(action.data);
const instance = new Assignment(action.data);
return listUpdater(state, instance);
case ADD_MESSAGE.SUCCESS:
const user = users.filter(user => user.id === action.data.author).first();
const activity = new Activity({ type: 'message', user, ...action.data,});
return state.updateIn([ 'activeIssue', 'activities' ], list => list.push(activity));
return state.update([ 'activeIssue' ], issue => issue.activities.push(activity));
default:
return state;
}
@ -79,7 +79,7 @@ export default withRequestState({
export const init = createInit(name);
export const edit = createEdit(name);
export function fetchProjects(sessionId) {
export function fetchProjects() {
return {
types: FETCH_PROJECTS.toArray(),
call: client => client.get(`/integrations/issues/list_projects`)
@ -100,13 +100,6 @@ export function fetchAssignments(sessionId) {
}
}
export function fetchAssigment(sessionId, id) {
return {
types: FETCH_ASSIGNMENT.toArray(),
call: client => client.get(`/sessions/${ sessionId }/assign/${ id }`)
}
}
export function addActivity(sessionId, params) {
const data = { ...params, assignee: params.assignee, issueType: params.issueType }
return {

View file

@ -9,7 +9,6 @@ import { createInit, createEdit } from './funcTools/crud';
const idKey = 'id';
const name = 'assignment';
const listUpdater = createListUpdater(idKey);
const itemInListUpdater = createItemInListUpdater(idKey);
const FETCH_ASSIGNMENTS = new RequestTypes('asignment/FETCH_ASSIGNMENTS');
const FETCH_ISSUE = new RequestTypes('asignment/FETCH_ISSUE');
@ -23,8 +22,8 @@ const RESET_ACTIVE_ISSUE = 'assignment/RESET_ACTIVE_ISSUE';
const initialState = Map({
list: List(),
instance: Assignment(),
activeIssue: Assignment(),
instance: new Assignment(),
activeIssue: new Assignment(),
issueTypes: List(),
issueTypeIcons: Set(),
users: List(),
@ -39,9 +38,9 @@ const reducer = (state = initialState, action = {}) => {
case FETCH_PROJECTS.SUCCESS:
return state.set('projects', List(action.data));
case FETCH_ASSIGNMENTS.SUCCESS:
return state.set('list', List(action.data).map(Assignment));
return state.set('list', action.data.map(as => new Assignment(as)));
case ADD_ACTIVITY.SUCCESS:
const instance = Assignment(action.data);
const instance = new Assignment(action.data);
return listUpdater(state, instance);
case FETCH_META.SUCCESS:
issueTypes = action.data.issueTypes;
@ -53,16 +52,16 @@ const reducer = (state = initialState, action = {}) => {
.set('users', List(action.data.users))
.set('issueTypeIcons', issueTypeIcons)
case FETCH_ISSUE.SUCCESS:
return state.set('activeIssue', Assignment({ ...action.data, users}));
return state.set('activeIssue', new Assignment({ ...action.data, users}));
case RESET_ACTIVE_ISSUE:
return state.set('activeIssue', Assignment());
return state.set('activeIssue', new Assignment());
case ADD_MESSAGE.SUCCESS:
const user = users.filter(user => user.id === action.data.author).first();
const activity = new Activity({ type: 'message', user, ...action.data,});
return state.updateIn([ 'activeIssue', 'activities' ], list => list.push(activity));
case INIT:
action.instance.issueType = issueTypes.length > 0 ? issueTypes[0].id : '';
return state.set('instance', Assignment(action.instance));
return state.set('instance', new Assignment(action.instance));
case EDIT:
return state.mergeIn([ 'instance' ], action.instance);
default:
@ -101,13 +100,6 @@ export function fetchProjects() {
}
}
export function fetchIssue(sessionId, id) {
return {
types: FETCH_ISSUE.toArray(),
call: client => client.get(`/sessions/${ sessionId }/assign/jira/${ id }`)
}
}
export function fetchMeta(projectId) {
return {
types: FETCH_META.toArray(),

View file

@ -1,6 +1,7 @@
import { List, Map } from 'immutable';
import Session from 'Types/session';
import ErrorStack from 'Types/session/errorStack';
import { Location, InjectedEvent } from 'Types/session/event'
import Watchdog from 'Types/watchdog';
import { clean as cleanParams } from 'App/api_client';
import withRequestState, { RequestTypes } from './requestStateCreator';
@ -46,7 +47,7 @@ const defaultDateFilters = {
};
const initObj = {
list: List(),
list: [],
sessionIds: [],
current: new Session(),
total: 0,
@ -61,7 +62,7 @@ const initObj = {
filteredEvents: null,
eventsQuery: '',
showChatWindow: false,
liveSessions: List(),
liveSessions: [],
visitedEvents: List(),
insights: List(),
insightFilters: defaultDateFilters,
@ -76,7 +77,12 @@ const initObj = {
const initialState = Map(initObj);
const reducer = (state = initialState, action = {}) => {
interface IAction extends Record<string, any>{
type: string;
data: any;
}
const reducer = (state = initialState, action: IAction) => {
switch (action.type) {
case FETCH_ERROR_STACK.SUCCESS:
return state.set('errorStack', List(action.data.trace).map(es => new ErrorStack(es))).set('sourcemapUploaded', action.data.sourcemapUploaded);
@ -87,13 +93,11 @@ const reducer = (state = initialState, action = {}) => {
const { sessions, total } = action.data;
const list = sessions.map(s => new Session(s));
console.log(sessions, list, action)
return state
.set('list', list)
.set('sessionIds', list.map(({ sessionId }) => sessionId).toJS())
.set(
'favoriteList',
list.filter(({ favorite }) => favorite)
)
.set('sessionIds', list.map(({ sessionId }) => sessionId))
.set('favoriteList', list.filter(({ favorite }) => favorite))
.set('total', total);
case FETCH_AUTOPLAY_LIST.SUCCESS:
let sessionIds = state.get('sessionIds');
@ -125,13 +129,13 @@ const reducer = (state = initialState, action = {}) => {
const events = action.filter.events;
const session = new Session(action.data);
const matching = [];
const matching: number[] = [];
const visitedEvents = [];
const tmpMap = {};
const visitedEvents: Location[] = [];
const tmpMap = new Set();
session.events.forEach((event) => {
if (event.type === 'LOCATION' && !tmpMap.hasOwnProperty(event.url)) {
tmpMap[event.url] = event.url;
if (event.type === 'LOCATION' && !tmpMap.has(event.url)) {
tmpMap.add(event.url);
visitedEvents.push(event);
}
});
@ -156,7 +160,7 @@ const reducer = (state = initialState, action = {}) => {
.set('host', visitedEvents[0] && visitedEvents[0].host);
}
case FETCH_FAVORITE_LIST.SUCCESS:
return state.set('favoriteList', List(action.data).map(s => new Session(s)));
return state.set('favoriteList', action.data.map(s => new Session(s)));
case TOGGLE_FAVORITE.SUCCESS: {
const id = action.sessionId;
let mutableState = state
@ -186,7 +190,7 @@ const reducer = (state = initialState, action = {}) => {
diff = diff === 0 ? s1.startedAt - s2.startedAt : diff;
return action.sign * diff;
};
return state.update('list', (list) => list.sort(comparator)).update('favoriteList', (list) => list.sort(comparator));
return state.update('list', (list: Session[]) => list.sort(comparator)).update('favoriteList', (list: Session[]) => list.sort(comparator));
}
case REDEFINE_TARGET: {
// TODO: update for list

View file

@ -1,47 +0,0 @@
import Record from 'Types/Record';
import Activity from './activity';
import { List } from 'immutable';
import { DateTime } from 'luxon';
import { validateName, notEmptyString } from 'App/validate';
export default Record({
id: undefined,
title: '',
timestamp: undefined,
creatorId: undefined,
sessionId: undefined,
projectId: '',
siteId: undefined,
activities: List(),
closed: false,
assignee: '',
commentsCount: undefined,
issueType: '',
description: '',
iconUrl: ''
}, {
fromJS: (assignment) => ({
...assignment,
timestamp: assignment.createdAt ? DateTime.fromISO(assignment.createdAt) : undefined,
activities: assignment.comments ? List(assignment.comments).map(activity => {
if (assignment.users) {
activity.user = assignment.users.filter(user => user.id === activity.author).first();
}
return new Activity(activity)
}) : List()
}),
methods: {
validate: function() {
return !!this.projectId && !!this.issueType &&
notEmptyString(this.title) && notEmptyString(this.description)
},
toCreate: function() {
return {
title: this.title,
description: this.description,
assignee: this.assignee,
issueType: this.issueType
}
}
}
})

View file

@ -0,0 +1,77 @@
import Activity, { IActivity } from './activity';
import { DateTime } from 'luxon';
import { notEmptyString } from 'App/validate';
interface IAssignment {
id: string;
title: string;
timestamp: number;
creatorId: string;
sessionId: string;
projectId: string;
siteId: string;
activities: [];
closed: boolean;
assignee: string;
commentsCount: number;
issueType: string;
description: string;
iconUrl: string;
createdAt?: string;
comments: IActivity[]
users: { id: string }[]
}
export default class Assignment {
id: IAssignment["id"];
title: IAssignment["title"] = '';
timestamp: IAssignment["timestamp"];
creatorId: IAssignment["creatorId"];
sessionId: IAssignment["sessionId"];
projectId: IAssignment["projectId"] = '';
siteId: IAssignment["siteId"];
activities: IAssignment["activities"];
closed: IAssignment["closed"];
assignee: IAssignment["assignee"] = '';
commentsCount: IAssignment["commentsCount"];
issueType: IAssignment["issueType"] = '';
description: IAssignment["description"] = '';
iconUrl: IAssignment["iconUrl"] = '';
constructor(assignment?: IAssignment) {
if (assignment) {
Object.assign(this, {
...assignment,
timestamp: assignment.createdAt ? DateTime.fromISO(assignment.createdAt) : undefined,
activities: assignment.comments ? assignment.comments.map(activity => {
if (assignment.users) {
// @ts-ignore ???
activity.user = assignment.users.filter(user => user.id === activity.author)[0];
}
return new Activity(activity)
}) : []
})
}
}
toJS() {
return this
}
validate() {
return !!this.projectId && !!this.issueType && notEmptyString(this.title) && notEmptyString(this.description)
}
get isValid() {
return !!this.projectId && !!this.issueType && notEmptyString(this.title) && notEmptyString(this.description)
}
toCreate() {
return {
title: this.title,
description: this.description,
assignee: this.assignee,
issueType: this.issueType
}
}
}

View file

@ -10,7 +10,7 @@ function getStck0InfoString(stack: Stack) {
return s;
}
type Stack = { function: string; url: string}[]
type Stack = { function: string; url: string }[]
export interface IError {
sessionId: string

View file

@ -15,7 +15,7 @@ export default class ErrorStack {
lineNo: IErrorStack["lineNo"]
colNo: IErrorStack["colNo"]
offset: IErrorStack["offset"]
context:IErrorStack["context"]
context: IErrorStack["context"]
constructor(es: IErrorStack) {
Object.assign(this, {

View file

@ -111,7 +111,7 @@ class Input extends Event {
}
class Location extends Event {
export class Location extends Event {
readonly name = 'Location';
readonly type = LOCATION;
url: LocationEvent["url"]

View file

@ -12,7 +12,7 @@ const OTHER = 'other' as const;
function getResourceStatus(status: number, success: boolean) {
if (status != null) return String(status);
if (typeof success === 'boolean' || typeof success === 'number') {
return !!success
return !!success
? '2xx-3xx'
: '4xx-5xx';
}
@ -20,8 +20,12 @@ function getResourceStatus(status: number, success: boolean) {
}
function getResourceSuccess(success: boolean, status: number) {
if (success != null) { return !!success }
if (status != null) { return status < 400 }
if (success != null) {
return !!success
}
if (status != null) {
return status < 400
}
return true
}
@ -56,7 +60,7 @@ interface IResource {
success: boolean,
score: number,
method: string,
request:string,
request: string,
response: string,
headerSize: number,
encodedBodySize: number,

View file

@ -8,6 +8,7 @@ import { Note } from 'App/services/NotesService'
const HASH_MOD = 1610612741;
const HASH_P = 53;
function hashString(s: string): number {
let mul = 1;
let hash = 0;
@ -192,7 +193,7 @@ export default class Session {
const isMobile = ['console', 'mobile', 'tablet'].includes(userDeviceType);
const events: InjectedEvent[] = []
const rawEvents: (EventData & { key: number})[] = []
const rawEvents: (EventData & { key: number })[] = []
if (session.events?.length) {
(session.events as EventData[]).forEach((event: EventData, k) => {
@ -224,12 +225,13 @@ export default class Session {
const exceptions = (errors as IError[]).map(e => new SessionError(e)) || [];
const issuesList = (issues as IIssue[]).map((i, k) => new Issue({ ...i, time: i.timestamp - startedAt, key: k })) || [];
const issuesList = (issues as IIssue[]).map(
(i, k) => new Issue({ ...i, time: i.timestamp - startedAt, key: k })) || [];
const rawNotes = notes;
const notesWithEvents = [...rawEvents, ...rawNotes].sort((a, b) => {
// @ts-ignore just in case
const aTs = a.timestamp || a.time;
const aTs = a.timestamp || a.time;
// @ts-ignore
const bTs = b.timestamp || b.time;

View file

@ -9,62 +9,62 @@ export const CLOUDWATCH = 'cloudwatch';
export const ELASTICSEARCH = 'elasticsearch';
export const SUMOLOGIC = 'sumologic';
export const typeList = [ OPENREPLAY, SENTRY, DATADOG, STACKDRIVER, ROLLBAR, BUGSNAG, CLOUDWATCH, ELASTICSEARCH, SUMOLOGIC ];
export const typeList = [OPENREPLAY, SENTRY, DATADOG, STACKDRIVER, ROLLBAR, BUGSNAG, CLOUDWATCH, ELASTICSEARCH, SUMOLOGIC];
export function isRed(event: StackEvent) {
if (!event.payload) return false;
switch(event.source) {
case SENTRY:
return event.payload['event.type'] === 'error';
case DATADOG:
return true;
case STACKDRIVER:
return false;
case ROLLBAR:
return true;
case NEWRELIC:
return true;
case BUGSNAG:
return true;
case CLOUDWATCH:
return true;
case SUMOLOGIC:
return false;
default:
return event.level === 'error';
}
if (!event.payload) return false;
switch (event.source) {
case SENTRY:
return event.payload['event.type'] === 'error';
case DATADOG:
return true;
case STACKDRIVER:
return false;
case ROLLBAR:
return true;
case NEWRELIC:
return true;
case BUGSNAG:
return true;
case CLOUDWATCH:
return true;
case SUMOLOGIC:
return false;
default:
return event.level === 'error';
}
}
export interface IStackEvent {
time: number;
timestamp: number;
index: number;
name: string;
message: string;
payload: any;
source: any;
level: string;
time: number;
timestamp: number;
index: number;
name: string;
message: string;
payload: any;
source: any;
level: string;
isRed: () => boolean;
isRed: () => boolean;
}
export default class StackEvent {
time: IStackEvent["time"]
index: IStackEvent["index"];
name: IStackEvent["name"];
message: IStackEvent["message"];
payload: IStackEvent["payload"];
source: IStackEvent["source"];
level: IStackEvent["level"];
time: IStackEvent["time"]
index: IStackEvent["index"];
name: IStackEvent["name"];
message: IStackEvent["message"];
payload: IStackEvent["payload"];
source: IStackEvent["source"];
level: IStackEvent["level"];
constructor(evt: IStackEvent) {
Object.assign(this, {
...evt,
source: evt.source || OPENREPLAY
});
}
constructor(evt: IStackEvent) {
Object.assign(this, {
...evt,
source: evt.source || OPENREPLAY
});
}
isRed() {
return isRed(this);
}
isRed() {
return isRed(this);
}
}