Compare commits

...
Sign in to create a new pull request.

37 commits

Author SHA1 Message Date
Shekar Siri
cca4478c6a fix(ui): path analysis showing multi series 2024-12-09 13:04:20 +01:00
Shekar Siri
fdf08b3fa1 fix(ui): card filters restriction for path analysis 2024-12-09 12:40:05 +01:00
Mehdi Osman
fc48ba4149
Increment chalice chart version (#2815)
Co-authored-by: GitHub Action <action@github.com>
2024-12-04 13:10:03 +01:00
Kraiem Taha Yassine
04db322e54
fix(chalice): support webhook default ports (#2814)
* fix(chalice): support webhook default ports

* fix(chalice): support webhook default ports EE
2024-12-04 13:05:38 +01:00
Mehdi Osman
c9ea3651db
Increment frontend chart version (#2813)
Co-authored-by: GitHub Action <action@github.com>
2024-12-03 17:16:31 +01:00
Shekar Siri
1dc63bf88b
change(ui): webhooks url allow port (#2812) 2024-12-03 17:09:06 +01:00
Gabriele Angrisani
2fb7b3d542
fix redis volume reference folder (#2805) 2024-12-03 16:33:18 +01:00
Mehdi Osman
57a21eb31d
Increment chalice chart version (#2811)
Co-authored-by: GitHub Action <action@github.com>
2024-12-03 16:30:38 +01:00
Kraiem Taha Yassine
e9a1a8c4eb
fix(chalice): fixed edit user's role (#2810) 2024-12-03 16:27:42 +01:00
Mehdi Osman
14191c1de4
Increment chalice chart version (#2807)
Co-authored-by: GitHub Action <action@github.com>
2024-12-03 15:36:33 +01:00
Mehdi Osman
7e52c97d62
Increment frontend chart version (#2809)
Co-authored-by: GitHub Action <action@github.com>
2024-12-03 15:35:53 +01:00
Shekar Siri
1cdb9bd06d
fix(ui): do not allow protected roles (#2808) 2024-12-03 14:15:38 +01:00
Kraiem Taha Yassine
e7ad4c8bd0
fix(chalice): support session's search null duration (#2806) 2024-12-03 13:24:24 +01:00
Mehdi Osman
29d69e5b24
Increment chalice chart version (#2804)
Co-authored-by: GitHub Action <action@github.com>
2024-12-02 23:14:07 +01:00
Kraiem Taha Yassine
2e5517509b
fix(chalice): fixed accept invitation response (#2803) 2024-12-02 23:06:41 +01:00
rjshrjndrn
c95a4f6770 ci(action): show latest build log 2024-12-02 21:21:32 +01:00
Mehdi Osman
8af7d1a263
Increment frontend chart version (#2801)
Co-authored-by: GitHub Action <action@github.com>
2024-12-02 21:01:08 +01:00
Shekar Siri
332cbb3516
fix(ui): 403 clear the token (#2800) 2024-12-02 20:55:05 +01:00
Mehdi Osman
1b564f53d5
Increment frontend chart version (#2799)
Co-authored-by: GitHub Action <action@github.com>
2024-12-02 18:24:57 +01:00
Shekar Siri
1aa3b4b4e5
fix(ui): trigger session search on change (#2798) 2024-12-02 18:17:03 +01:00
Mehdi Osman
d531b5da7e
Increment frontend chart version (#2790)
Co-authored-by: GitHub Action <action@github.com>
2024-11-29 16:50:02 +01:00
Shekar Siri
e173591d88
fix(assist): pagination (#2789) 2024-11-29 15:44:45 +01:00
Mehdi Osman
359ecc85af
Increment frontend chart version (#2786)
Co-authored-by: GitHub Action <action@github.com>
2024-11-26 13:11:22 +01:00
Shekar Siri
f0e8100283
fix(ui) - sessions refresh and navigation (#2785)
* change(ui): search query params improvements

* fix(ui): sessions, bookmark, notes navigation and search silters and timestamp issues
2024-11-26 13:03:58 +01:00
Mehdi Osman
251d727375
Increment chalice chart version (#2783)
Co-authored-by: GitHub Action <action@github.com>
2024-11-25 17:42:22 +01:00
Kraiem Taha Yassine
b00a90484e
fix(chalice): support user-city for assist (#2782) 2024-11-25 17:35:51 +01:00
Mehdi Osman
ce0686eec3
Increment frontend chart version (#2780)
Co-authored-by: GitHub Action <action@github.com>
2024-11-25 14:49:59 +01:00
Shekar Siri
34232ed23c
Fix sessions list (#2779)
* fix(ui): sessions list persist page, show latest sessions

* fix(ui): latest sessions check clear the list
2024-11-25 13:43:07 +01:00
Mehdi Osman
954bfbf8f7
Increment frontend chart version (#2778)
Co-authored-by: GitHub Action <action@github.com>
2024-11-25 12:22:33 +01:00
Shekar Siri
c0197cdfeb
fix(ui): sessions list persist page, show latest sessions (#2777) 2024-11-25 12:12:16 +01:00
Andrés Carrillo
12ab110e0e
Update common.env (#2776) 2024-11-23 10:24:31 -05:00
Mehdi Osman
f48808f42e
Increment frontend chart version (#2774)
Co-authored-by: GitHub Action <action@github.com>
2024-11-22 14:15:28 +01:00
Delirium
b080a98764
ui: fix ws panel crash (#2773) 2024-11-22 14:07:42 +01:00
Mehdi Osman
dd885c65ac
Increment frontend chart version (#2770)
Co-authored-by: GitHub Action <action@github.com>
2024-11-20 15:35:32 +01:00
Shekar Siri
0ad2836650
cherry-pick(ui): 54abbe58a (#2769) 2024-11-20 14:02:42 +01:00
Mehdi Osman
20b76a0ed9
Updated patch build from main 884f3499ef (#2768)
* Increment chalice chart version

* Increment alerts chart version

---------

Co-authored-by: GitHub Action <action@github.com>
2024-11-20 12:45:49 +01:00
Kraiem Taha Yassine
884f3499ef
fix(chalice): support top graphql autocomplete (#2767)
refactor(chalice): enforce UTC TZ
refactor(crons): enforce UTC TZ
refactor(alerts): enforce UTC TZ
2024-11-20 12:34:25 +01:00
46 changed files with 493 additions and 308 deletions

View file

@ -83,6 +83,7 @@ jobs:
[ -d $MSAAS_REPO_FOLDER ] || {
git clone -b dev --recursive https://x-access-token:$MSAAS_REPO_CLONE_TOKEN@$MSAAS_REPO_URL $MSAAS_REPO_FOLDER
cd $MSAAS_REPO_FOLDER
git log -1
cd openreplay && git fetch origin && git checkout main # This have to be changed to specific tag
git log -1
cd $MSAAS_REPO_FOLDER

View file

@ -457,12 +457,6 @@ def set_password_invitation(user_id, new_password):
user = update(tenant_id=-1, user_id=user_id, changes=changes)
r = authenticate(user['email'], new_password)
tenant_id = r.pop("tenantId")
r["limits"] = {
"teamMember": -1,
"projects": -1,
"metadata": metadata.get_remaining_metadata_with_count(tenant_id)}
return {
"jwt": r.pop("jwt"),
"refreshToken": r.pop("refreshToken"),
@ -470,10 +464,7 @@ def set_password_invitation(user_id, new_password):
"spotJwt": r.pop("spotJwt"),
"spotRefreshToken": r.pop("spotRefreshToken"),
"spotRefreshTokenMaxAge": r.pop("spotRefreshTokenMaxAge"),
'data': {
"scopeState": scope.get_scope(-1),
"user": r
}
**r
}

View file

@ -129,13 +129,13 @@ def add_edit(tenant_id, data: schemas.WebhookSchema, replace_none=None):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"name already exists.")
if data.webhook_id is not None:
return update(tenant_id=tenant_id, webhook_id=data.webhook_id,
changes={"endpoint": data.endpoint.unicode_string(),
changes={"endpoint": data.endpoint,
"authHeader": data.auth_header,
"name": data.name},
replace_none=replace_none)
else:
return add(tenant_id=tenant_id,
endpoint=data.endpoint.unicode_string(),
endpoint=data.endpoint,
auth_header=data.auth_header,
name=data.name,
replace_none=replace_none)

View file

@ -1,3 +1,4 @@
#!/bin/sh
export TZ=UTC
uvicorn app:app --host 0.0.0.0 --port $LISTEN_PORT --proxy-headers --log-level ${S_LOGLEVEL:-warning}

View file

@ -1,3 +1,4 @@
#!/bin/sh
export TZ=UTC
export ASSIST_KEY=ignore
uvicorn app:app --host 0.0.0.0 --port 8888 --log-level ${S_LOGLEVEL:-warning}

View file

@ -1,3 +1,4 @@
#!/bin/zsh
export TZ=UTC
uvicorn app_alerts:app --reload --port 8888 --log-level ${S_LOGLEVEL:-warning}

View file

@ -1,3 +1,4 @@
#!/bin/zsh
export TZ=UTC
uvicorn app:app --reload --log-level ${S_LOGLEVEL:-warning}

View file

@ -211,7 +211,8 @@ class IssueTrackingJiraSchema(IssueTrackingIntegration):
class WebhookSchema(BaseModel):
webhook_id: Optional[int] = Field(default=None)
endpoint: AnyHttpUrl = Field(...)
processed_endpoint: AnyHttpUrl = Field(..., alias="endpoint")
endpoint: Optional[str] = Field(default=None, doc_hidden=True)
auth_header: Optional[str] = Field(default=None)
name: str = Field(default="", max_length=100, pattern=NAME_PATTERN)
@ -754,6 +755,8 @@ class SessionsSearchPayloadSchema(_TimedSchema, _PaginatedSchema):
for f in values.get("filters", []):
vals = []
for v in f.get("value", []):
if f.get("type", "") == FilterType.DURATION.value and v is None:
v = 0
if v is not None and (f.get("type", "") != FilterType.DURATION.value
or str(v).isnumeric()):
vals.append(v)
@ -1357,6 +1360,7 @@ class LiveFilterType(str, Enum):
USER_BROWSER = FilterType.USER_BROWSER.value
USER_DEVICE = FilterType.USER_DEVICE.value
USER_COUNTRY = FilterType.USER_COUNTRY.value
USER_CITY = FilterType.USER_CITY.value
USER_STATE = FilterType.USER_STATE.value
USER_ID = FilterType.USER_ID.value
USER_ANONYMOUS_ID = FilterType.USER_ANONYMOUS_ID.value

View file

@ -182,3 +182,20 @@ def delete(tenant_id, user_id, role_id):
{"tenant_id": tenant_id, "role_id": role_id})
cur.execute(query=query)
return get_roles(tenant_id=tenant_id)
def get_role(tenant_id, role_id):
with pg_client.PostgresClient() as cur:
query = cur.mogrify("""SELECT roles.*
FROM public.roles
WHERE tenant_id =%(tenant_id)s
AND deleted_at IS NULL
AND not service_role
AND role_id = %(role_id)s
LIMIT 1;""",
{"tenant_id": tenant_id, "role_id": role_id})
cur.execute(query=query)
row = cur.fetchone()
if row is not None:
row["created_at"] = TimeUTC.datetime_to_timestamp(row["created_at"])
return helper.dict_to_camel_case(row)

View file

@ -199,6 +199,12 @@ def create_member(tenant_id, user_id, data: schemas.CreateMemberSchema, backgrou
role_id = data.roleId
if role_id is None:
role_id = roles.get_role_by_name(tenant_id=tenant_id, name="member").get("roleId")
else:
role = roles.get_role(tenant_id=tenant_id, role_id=role_id)
if role is None:
return {"errors": ["role not found"]}
if role["name"].lower() == "owner" and role["protected"]:
return {"errors": ["invalid role"]}
invitation_token = __generate_invitation_token()
user = get_deleted_user_by_email(email=data.email)
if user is not None and user["tenantId"] == tenant_id:
@ -333,7 +339,7 @@ def edit_member(user_id_to_update, tenant_id, changes: schemas.EditMemberSchema,
if editor_id != user_id_to_update:
admin = get_user_role(tenant_id=tenant_id, user_id=editor_id)
if not admin["superAdmin"] and not admin["admin"]:
return {"errors": ["unauthorized"]}
return {"errors": ["unauthorized, you must have admin privileges"]}
if admin["admin"] and user["superAdmin"]:
return {"errors": ["only the owner can edit his own details"]}
else:
@ -343,10 +349,10 @@ def edit_member(user_id_to_update, tenant_id, changes: schemas.EditMemberSchema,
return {"errors": ["cannot change your own admin privileges"]}
if changes.roleId:
if user["superAdmin"] and changes.roleId != user["roleId"]:
changes.roleId = None
return {"errors": ["owner's role cannot be changed"]}
if changes.roleId != user["roleId"]:
elif user["superAdmin"]:
changes.roleId = None
elif changes.roleId != user["roleId"]:
return {"errors": ["cannot change your own role"]}
if changes.name and len(changes.name) > 0:
@ -357,6 +363,12 @@ def edit_member(user_id_to_update, tenant_id, changes: schemas.EditMemberSchema,
if changes.roleId is not None:
_changes["roleId"] = changes.roleId
role = roles.get_role(tenant_id=tenant_id, role_id=changes.roleId)
if role is None:
return {"errors": ["role not found"]}
else:
if role["name"].lower() == "owner" and role["protected"]:
return {"errors": ["invalid role"]}
if len(_changes.keys()) > 0:
update(tenant_id=tenant_id, user_id=user_id_to_update, changes=_changes, output=False)
@ -540,12 +552,6 @@ def set_password_invitation(tenant_id, user_id, new_password):
user = update(tenant_id=tenant_id, user_id=user_id, changes=changes)
r = authenticate(user['email'], new_password)
tenant_id = r.pop("tenantId")
r["limits"] = {
"teamMember": -1,
"projects": -1,
"metadata": metadata.get_remaining_metadata_with_count(tenant_id)}
return {
"jwt": r.pop("jwt"),
"refreshToken": r.pop("refreshToken"),
@ -554,10 +560,7 @@ def set_password_invitation(tenant_id, user_id, new_password):
"spotRefreshToken": r.pop("spotRefreshToken"),
"spotRefreshTokenMaxAge": r.pop("spotRefreshTokenMaxAge"),
"tenantId": tenant_id,
'data': {
"scopeState": scope.get_scope(tenant_id),
"user": r
}
**r
}

View file

@ -136,13 +136,13 @@ def add_edit(tenant_id, data: schemas.WebhookSchema, replace_none=None):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"name already exists.")
if data.webhook_id is not None:
return update(tenant_id=tenant_id, webhook_id=data.webhook_id,
changes={"endpoint": data.endpoint.unicode_string(),
changes={"endpoint": data.endpoint,
"authHeader": data.auth_header,
"name": data.name},
replace_none=replace_none)
else:
return add(tenant_id=tenant_id,
endpoint=data.endpoint.unicode_string(),
endpoint=data.endpoint,
auth_header=data.auth_header,
name=data.name,
replace_none=replace_none)

View file

@ -58,6 +58,7 @@ def get_event_type(event_type: Union[schemas.EventType, schemas.PerformanceEvent
schemas.EventType.REQUEST: "REQUEST",
schemas.EventType.REQUEST_DETAILS: "REQUEST",
schemas.PerformanceEventType.FETCH_FAILED: "REQUEST",
schemas.GraphqlFilterType.GRAPHQL_NAME: "GRAPHQL",
schemas.EventType.STATE_ACTION: "STATEACTION",
schemas.EventType.ERROR: "ERROR",
schemas.PerformanceEventType.LOCATION_AVG_CPU_LOAD: 'PERFORMANCE',

View file

@ -1,4 +1,5 @@
#!/bin/sh
export TZ=UTC
sh env_vars.sh
source /tmp/.env.override

View file

@ -1,4 +1,5 @@
#!/bin/sh
export TZ=UTC
export ASSIST_KEY=ignore
sh env_vars.sh
source /tmp/.env.override

View file

@ -1,4 +1,5 @@
#!/bin/sh
export TZ=UTC
export ASSIST_KEY=ignore
sh env_vars.sh
source /tmp/.env.override

View file

@ -198,6 +198,11 @@ export default class APIClient {
}
return fetch(edp + _path, init).then((response) => {
if (response.status === 403) {
console.warn('API returned 403. Clearing JWT token.');
this.onUpdateJwt({ jwt: undefined }); // Clear the token
}
if (response.ok) {
return response;
} else {

View file

@ -18,7 +18,7 @@ function UserForm() {
const isSaving = userStore.saving;
const user: any = userStore.instance || userStore.initUser();
const roles = roleStore.list
.filter((r) => (r.isProtected ? user.isSuperAdmin : true))
.filter((r) => (r.protected ? user.isSuperAdmin : true))
.map((r) => ({ label: r.name, value: r.roleId }));
const onChangeCheckbox = (e: any) => {

View file

@ -41,6 +41,7 @@ function ExcludeFilters(props: Props) {
onRemoveFilter={() => onRemoveFilter(index)}
// saveRequestPayloads={saveRequestPayloads}
disableDelete={false}
allowedFilterKeys={[FilterKey.LOCATION, FilterKey.CLICK, FilterKey.INPUT, FilterKey.CUSTOM]}
// excludeFilterKeys={excludeFilterKeys}
/>
))}

View file

@ -136,9 +136,10 @@ function FilterSeries(props: Props) {
toggleExpand={() => setExpanded(!expanded)}/>
)}
{expandable && (
{expandable && !expanded && (
<Space className="justify-between w-full px-5 py-2 cursor-pointer" onClick={() => setExpanded(!expanded)}>
<div>{!expanded && <FilterCountLabels filters={series.filter.filters} toggleExpand={() => setExpanded(!expanded)}/>}</div>
<FilterCountLabels filters={series.filter.filters} toggleExpand={() => setExpanded(!expanded)}/>
{/*<div>{!expanded && <FilterCountLabels filters={series.filter.filters} toggleExpand={() => setExpanded(!expanded)}/>}</div>*/}
<Button size="small"
icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
</Space>
@ -156,13 +157,13 @@ function FilterSeries(props: Props) {
supportsEmpty={supportsEmpty}
onFilterMove={onFilterMove}
excludeFilterKeys={excludeFilterKeys}
// actions={[
// expandable && (
// <Button onClick={() => setExpanded(!expanded)}
// size="small"
// icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
// )
// ]}
actions={[
expandable && (
<Button onClick={() => setExpanded(!expanded)}
size="small"
icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
)
]}
/>
) : (
<div className="color-gray-medium">{emptyMessage}</div>

View file

@ -68,7 +68,7 @@ const FilterSection = observer(({ metric, excludeFilterKeys }: any) => {
const eventsLength = metric.series[0].filter.filters.filter((i: any) => i && i.isEvent).length;
// const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1);
const isSingleSeries = isTable || isFunnel || isClickMap || isInsights || isRetention;
const isSingleSeries = isTable || isFunnel || isClickMap || isInsights || isRetention || isPathAnalysis;
// const onAddFilter = (filter: any) => {
// metric.series[0].filter.addFilter(filter);

View file

@ -12,6 +12,8 @@ import { withRouter, RouteComponentProps, useLocation } from 'react-router-dom';
import FlagView from 'Components/FFlags/FlagView/FlagView';
import { observer } from 'mobx-react-lite';
import { useStore } from '@/mstore';
import NotesList from 'Shared/SessionsTabOverview/components/Notes/NoteList';
import NoteTags from 'Shared/SessionsTabOverview/components/Notes/NoteTags';
// @ts-ignore
interface IProps extends RouteComponentProps {
@ -36,15 +38,16 @@ function Overview({ match: { params } }: IProps) {
return (
<Switch>
<Route exact strict
path={[withSiteId(sessions(), siteId), withSiteId(notes(), siteId), withSiteId(bookmarks(), siteId)]}>
path={[withSiteId(sessions(), siteId), withSiteId(bookmarks(), siteId)]}>
<div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
<NoSessionsMessage siteId={siteId} />
<MainSearchBar />
<SessionSearch />
<div className="my-4" />
<SessionsTabOverview />
</div>
</Route>
<Route exact strict path={withSiteId(notes(), siteId)}>
<div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
<NotesList />
</div>
</Route>
<Route exact strict path={withSiteId(fflags(), siteId)}>
<FFlagsList siteId={siteId} />
</Route>

View file

@ -23,9 +23,9 @@ const lineLength = 40;
function WSPanel({ socketMsgList, onClose }: Props) {
const [query, setQuery] = React.useState('');
const [list, setList] = React.useState(socketMsgList);
const [selectedRow, setSelectedRow] = React.useState<SocketMsg | null>(null);
const [selectedRow, setSelectedRow] = React.useState<{ msg: SocketMsg, id: number } | null>(null);
const onQueryChange = (e) => {
const onQueryChange = (e: any) => {
setQuery(e.target.value);
const newList = filterList(socketMsgList, e.target.value, [
'data',
@ -69,15 +69,16 @@ function WSPanel({ socketMsgList, onClose }: Props) {
position: 'relative',
}}
>
{list.map((msg) => (
{list.map((msg, i) => (
<Row
msg={msg}
key={msg.timestamp}
onSelect={() => setSelectedRow(msg)}
onSelect={() => setSelectedRow({ msg, id: i })}
isSelected={selectedRow ? selectedRow.id === i : false}
/>
))}
{selectedRow ? (
<SelectedRow msg={selectedRow} onClose={() => setSelectedRow(null)} />
<SelectedRow msg={selectedRow.msg} onClose={() => setSelectedRow(null)} />
) : null}
</div>
</div>
@ -127,7 +128,7 @@ function MsgDirection({ dir }: { dir: 'up' | 'down' }) {
);
}
function Row({ msg, onSelect }: { msg: SocketMsg; onSelect: () => void }) {
function Row({ msg, onSelect, isSelected }: { msg: SocketMsg; isSelected: boolean; onSelect: () => void }) {
return (
<>
<div
@ -149,7 +150,7 @@ function Row({ msg, onSelect }: { msg: SocketMsg; onSelect: () => void }) {
'rounded-full font-bold text-xl p-2 bg-white w-6 h-6 flex items-center justify-center'
}
>
<span>{isOpen ? '-' : '+'}</span>
<span>{isSelected ? '-' : '+'}</span>
</div>
) : null}
</div>

View file

@ -24,7 +24,7 @@ function LiveSessionList() {
const totalLiveSessions = sessionStore.totalLiveSessions;
const loading = sessionStore.loadingLiveSessions;
const { currentPage } = searchStoreLive;
const metaList = customFieldStore.list
const metaList = customFieldStore.list;
const metaListLoading = customFieldStore.isLoading;
let timeoutId: any;
@ -32,7 +32,7 @@ function LiveSessionList() {
const hasUserFilter = filters.map((i: any) => i.key).includes(KEYS.USERID);
const sortOptions = [{ label: 'Start Time', value: 'timestamp' }].concat(
metaList
.map(({ key} : any) => ({
.map(({ key }: any) => ({
label: capitalize(key),
value: key
}))
@ -40,21 +40,33 @@ function LiveSessionList() {
useEffect(() => {
if (metaListLoading) return;
const _filter = { ...filter };
let shouldUpdate = false;
// Set default sort if not already set
if (sortOptions[1] && !filter.sort) {
_filter.sort = sortOptions[1].value;
shouldUpdate = true;
}
searchStoreLive.edit(_filter);
// Only update filters if there's a change
if (shouldUpdate) {
searchStoreLive.edit(_filter);
}
// Start auto-refresh timeout
timeout();
// Cleanup on component unmount or re-run
return () => {
clearTimeout(timeoutId);
};
}, [metaListLoading]);
}, [metaListLoading, filter.sort]); // Add necessary dependencies
const refetch = () => {
searchStoreLive.edit({ ...filter })
void searchStoreLive.fetchSessions();
}
};
const onUserClick = (userId: string, userAnonymousId: string) => {
if (userId) {
@ -66,7 +78,6 @@ function LiveSessionList() {
const onSortChange = ({ value }: any) => {
searchStoreLive.edit({ sort: value.value });
void searchStoreLive.fetchSessions();
};
const timeout = () => {
@ -102,8 +113,7 @@ function LiveSessionList() {
<div className="mx-2" />
<SortOrderButton
onChange={(state: any) => {
searchStoreLive.edit({ order: state })
void searchStoreLive.fetchSessions();
searchStoreLive.edit({ order: state });
}}
sortOrder={filter.order}
/>

View file

@ -22,7 +22,6 @@ function LiveSessionSearch() {
const onUpdateFilter = (filterIndex: number, filter: any) => {
searchStoreLive.updateFilter(filterIndex, filter);
void searchStoreLive.fetchSessions();
};
const onRemoveFilter = (filterIndex: number) => {
@ -33,16 +32,12 @@ function LiveSessionSearch() {
searchStoreLive.edit({
filters: newFilters
});
void searchStoreLive.fetchSessions();
};
const onChangeEventsOrder = (e: any, { name, value }: any) => {
searchStoreLive.edit({
eventsOrder: value
});
void searchStoreLive.fetchSessions();
};
return (hasEvents || hasFilters) ? (

View file

@ -11,50 +11,57 @@ import { useStore } from 'App/mstore';
import { debounce } from 'App/utils';
import useSessionSearchQueryHandler from 'App/hooks/useSessionSearchQueryHandler';
let debounceFetch: any = () => {
};
let debounceFetch: () => void;
function SessionSearch() {
const { tagWatchStore, aiFiltersStore, searchStore, customFieldStore, projectsStore } = useStore();
const appliedFilter = searchStore.instance;
const metaLoading = customFieldStore.isLoading;
const hasEvents = appliedFilter.filters.filter((i: any) => i.isEvent).length > 0;
const hasFilters = appliedFilter.filters.filter((i: any) => !i.isEvent).length > 0;
const hasEvents = appliedFilter.filters.some((i: any) => i.isEvent);
const hasFilters = appliedFilter.filters.some((i: any) => !i.isEvent);
const saveRequestPayloads = projectsStore.instance?.saveRequestPayloads ?? false;
useSessionSearchQueryHandler({
appliedFilter,
loading: metaLoading,
onBeforeLoad: async () => {
const tags = await tagWatchStore.getTags();
if (tags) {
addOptionsToFilter(
FilterKey.TAGGED_ELEMENT,
tags.map((tag) => ({
label: tag.name,
value: tag.tagId.toString()
}))
);
searchStore.refreshFilterOptions();
try {
const tags = await tagWatchStore.getTags();
if (tags) {
addOptionsToFilter(
FilterKey.TAGGED_ELEMENT,
tags.map((tag) => ({
label: tag.name,
value: tag.tagId.toString()
}))
);
searchStore.refreshFilterOptions();
}
} catch (error) {
console.error('Error during onBeforeLoad:', error);
}
}
});
useEffect(() => {
debounceFetch = debounce(() => searchStore.fetchSessions(), 500);
// void searchStore.fetchSessions(true)
}, []);
useEffect(() => {
if (searchStore.urlParsed) return;
debounceFetch();
}, [appliedFilter.filters]);
const onAddFilter = (filter: any) => {
searchStore.addFilter(filter);
debounceFetch();
};
const onUpdateFilter = (filterIndex: any, filter: any) => {
searchStore.updateFilter(filterIndex, filter);
debounceFetch();
};
const onFilterMove = (newFilters: any) => {
@ -85,49 +92,47 @@ function SessionSearch() {
};
const showPanel = hasEvents || hasFilters || aiFiltersStore.isLoading;
return !metaLoading ? (
<>
{showPanel ? (
<div className="border bg-white rounded-lg mt-4">
<div className="p-5">
{aiFiltersStore.isLoading ? (
<div className={'font-semibold flex items-center gap-2 mb-2'}>
<AnimatedSVG name={ICONS.LOADER} size={18} />
<span>Translating your query into search steps...</span>
</div>
) : null}
{hasEvents || hasFilters ? (
<FilterList
filter={appliedFilter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
onFilterMove={onFilterMove}
saveRequestPayloads={saveRequestPayloads}
/>
) : null}
</div>
{hasEvents || hasFilters ? (
<div className="border-t px-5 py-1 flex items-center -mx-2">
<div>
<FilterSelection filter={undefined} onFilterClick={onAddFilter}>
<Button variant="text-primary" className="mr-2" icon="plus">
ADD STEP
</Button>
</FilterSelection>
</div>
<div className="ml-auto flex items-center">
<SaveFilterButton />
</div>
</div>
) : null}
if (metaLoading) return null;
if (!showPanel) return null;
return (
<div className="border bg-white rounded-lg mt-4">
<div className="p-5">
{aiFiltersStore.isLoading ? (
<div className={'font-semibold flex items-center gap-2 mb-2'}>
<AnimatedSVG name={ICONS.LOADER} size={18} />
<span>Translating your query into search steps...</span>
</div>
) : null}
{hasEvents || hasFilters ? (
<FilterList
filter={appliedFilter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
onFilterMove={onFilterMove}
saveRequestPayloads={saveRequestPayloads}
/>
) : null}
</div>
{hasEvents || hasFilters ? (
<div className="border-t px-5 py-1 flex items-center -mx-2">
<div>
<FilterSelection filter={undefined} onFilterClick={onAddFilter}>
<Button variant="text-primary" className="mr-2" icon="plus">
ADD STEP
</Button>
</FilterSelection>
</div>
<div className="ml-auto flex items-center">
<SaveFilterButton />
</div>
</div>
) : (
<></>
)}
</>
) : null;
) : null}
</div>
);
}
export default observer(SessionSearch);

View file

@ -3,16 +3,17 @@ import React from 'react';
import { useStore } from 'App/mstore';
import LatestSessionsMessage from './components/LatestSessionsMessage';
import NotesList from './components/Notes/NoteList';
import SessionHeader from './components/SessionHeader';
import SessionList from './components/SessionList';
import { observer } from 'mobx-react-lite';
import NoSessionsMessage from 'Shared/NoSessionsMessage/NoSessionsMessage';
import MainSearchBar from 'Shared/MainSearchBar/MainSearchBar';
import SessionSearch from 'Shared/SessionSearch/SessionSearch';
function SessionsTabOverview() {
const [query, setQuery] = React.useState('');
const { aiFiltersStore, searchStore } = useStore();
const appliedFilter = searchStore.instance;
const isNotesRoute = searchStore.activeTab.type === 'notes';
const handleKeyDown = (event: any) => {
if (event.key === 'Enter') {
@ -25,25 +26,27 @@ function SessionsTabOverview() {
const testingKey = localStorage.getItem('__mauricio_testing_access') === 'true';
return (
<div className="widget-wrapper">
{testingKey ? (
<Input
value={query}
onKeyDown={handleKeyDown}
onChange={(e) => setQuery(e.target.value)}
className={'mb-2'}
placeholder={'ask session ai'}
/>
) : null}
<SessionHeader />
<div className="border-b" />
<LatestSessionsMessage />
{!isNotesRoute ? (
<>
<NoSessionsMessage />
<MainSearchBar />
<SessionSearch />
<div className="my-4" />
<div className="widget-wrapper">
{testingKey ? (
<Input
value={query}
onKeyDown={handleKeyDown}
onChange={(e) => setQuery(e.target.value)}
className={'mb-2'}
placeholder={'ask session ai'}
/>
) : null}
<SessionHeader />
<div className="border-b" />
<LatestSessionsMessage />
<SessionList />
) : (
<NotesList />
)}
</div>
</div>
</>
);
}

View file

@ -7,11 +7,16 @@ import { observer } from 'mobx-react-lite';
function LatestSessionsMessage() {
const { searchStore } = useStore();
const count = searchStore.latestList.size;
const onShowNewSessions = () => {
void searchStore.updateCurrentPage(1, true);
};
return count > 0 ? (
<div
className="bg-amber-50 p-1 flex w-full border-b text-center justify-center link"
style={{ backgroundColor: 'rgb(255 251 235)' }}
onClick={() => searchStore.updateCurrentPage(1)}
onClick={onShowNewSessions}
>
Show {numberWithCommas(count)} New {count > 1 ? 'Sessions' : 'Session'}
</div>

View file

@ -5,57 +5,70 @@ import NoteItem from './NoteItem';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import NoteTags from 'Shared/SessionsTabOverview/components/Notes/NoteTags';
function NotesList() {
const { notesStore } = useStore();
React.useEffect(() => {
void notesStore.fetchNotes();
void notesStore.fetchNotes();
}, [notesStore.page]);
const list = notesStore.notes;
return (
<Loader loading={notesStore.loading}>
<NoContent
show={list.length === 0}
title={
<div className="flex flex-col items-center justify-center">
{/* <Icon name="no-dashboard" size={80} color="figmaColors-accent-secondary" /> */}
<AnimatedSVG name={ICONS.NO_NOTES} size={60} />
<div className="text-center mt-4 text-lg font-medium">No notes yet</div>
</div>
}
subtext={
<div className="text-center flex justify-center items-center flex-col">
Note observations during session replays and share them with your team.
</div>
}
>
<div className="border-b rounded bg-white">
{list.map((note) => (
<React.Fragment key={note.noteId}>
<NoteItem note={note} />
</React.Fragment>
))}
</div>
<>
<div className="widget-wrapper">
<div className="flex items-center px-4 py-1 justify-between w-full">
<h2 className="text-2xl capitalize mr-4">Notes</h2>
<div className="w-full flex items-center justify-between py-4 px-6">
<div className="text-disabled-text">
Showing{' '}
<span className="font-semibold">{Math.min(list.length, notesStore.pageSize)}</span> out
of <span className="font-semibold">{notesStore.total}</span> notes
<div className="flex items-center justify-end w-full">
<NoteTags />
</div>
<Pagination
page={notesStore.page}
total={notesStore.total}
onPageChange={(page) => notesStore.changePage(page)}
limit={notesStore.pageSize}
debounceRequest={100}
/>
</div>
</NoContent>
</Loader>
<div className="border-b" />
<Loader loading={notesStore.loading}>
<NoContent
show={list.length === 0}
title={
<div className="flex flex-col items-center justify-center">
{/* <Icon name="no-dashboard" size={80} color="figmaColors-accent-secondary" /> */}
<AnimatedSVG name={ICONS.NO_NOTES} size={60} />
<div className="text-center mt-4 text-lg font-medium">No notes yet</div>
</div>
}
subtext={
<div className="text-center flex justify-center items-center flex-col">
Note observations during session replays and share them with your team.
</div>
}
>
<div className="border-b rounded bg-white">
{list.map((note) => (
<React.Fragment key={note.noteId}>
<NoteItem note={note} />
</React.Fragment>
))}
</div>
<div className="w-full flex items-center justify-between py-4 px-6">
<div className="text-disabled-text">
Showing{' '}
<span className="font-semibold">{Math.min(list.length, notesStore.pageSize)}</span> out
of <span className="font-semibold">{notesStore.total}</span> notes
</div>
<Pagination
page={notesStore.page}
total={notesStore.total}
onPageChange={(page) => notesStore.changePage(page)}
limit={notesStore.pageSize}
debounceRequest={100}
/>
</div>
</NoContent>
</Loader>
</div>
</>
);
}

View file

@ -2,7 +2,6 @@ import React, { useMemo } from 'react';
import Period from 'Types/app/period';
import SelectDateRange from 'Shared/SelectDateRange';
import SessionTags from '../SessionTags';
import NoteTags from '../Notes/NoteTags';
import SessionSort from '../SessionSort';
import { Space } from 'antd';
import { useStore } from 'App/mstore';
@ -17,9 +16,6 @@ function SessionHeader() {
const period = Period({ start: startDate, end: endDate, rangeName: rangeValue });
const title = useMemo(() => {
if (activeTab.type === 'notes') {
return 'Notes';
}
if (activeTab.type === 'bookmarks') {
return isEnterprise ? 'Vault' : 'Bookmarks';
}
@ -35,26 +31,15 @@ function SessionHeader() {
return (
<div className="flex items-center px-4 py-1 justify-between w-full">
<h2 className="text-2xl capitalize mr-4">{title}</h2>
{activeTab.type !== 'notes' ? (
<div className="flex items-center w-full justify-end">
{activeTab.type !== 'bookmarks' && (
<>
<SessionTags />
<div className="mr-auto" />
<Space>
<SelectDateRange isAnt period={period} onChange={onDateChange} right={true} />
<SessionSort />
</Space>
</>
)}
</div>
) : null}
{activeTab.type === 'notes' && (
<div className="flex items-center justify-end w-full">
<NoteTags />
</div>
)}
<div className="flex items-center w-full justify-end">
{activeTab.type !== 'bookmarks' && <SessionTags />}
<div className="mr-auto" />
<Space>
{activeTab.type !== 'bookmarks' &&
<SelectDateRange isAnt period={period} onChange={onDateChange} right={true} />}
<SessionSort />
</Space>
</div>
</div>
);
}

View file

@ -73,7 +73,6 @@ function SessionList() {
}, [isBookmark, isVault, activeTab, location.pathname]);
const [statusData, setStatusData] = React.useState<SessionStatus>({ status: 0, count: 0 });
const fetchStatus = async () => {
const response = await sessionService.getRecordingStatus();
setStatusData({

View file

@ -2,6 +2,8 @@
import { DateTime, Duration } from 'luxon'; // TODO
import { Timezone } from 'App/mstore/types/sessionSettings';
import { LAST_24_HOURS, LAST_30_DAYS, LAST_7_DAYS } from 'Types/app/period';
import { CUSTOM_RANGE } from '@/dateRange';
export function getDateFromString(date: string, format = 'yyyy-MM-dd HH:mm:ss:SSS'): string {
return DateTime.fromISO(date).toFormat(format);
@ -191,3 +193,35 @@ export const countDaysFrom = (timestamp: number): number => {
const d = new Date();
return Math.round(Math.abs(d.getTime() - date.toJSDate().getTime()) / (1000 * 3600 * 24));
}
export const getDateRangeUTC = (rangeName: string, customStartDate?: number, customEndDate?: number): {
startDate: number;
endDate: number
} => {
let endDate = new Date().getTime();
let startDate: number;
switch (rangeName) {
case LAST_7_DAYS:
startDate = endDate - 7 * 24 * 60 * 60 * 1000;
break;
case LAST_30_DAYS:
startDate = endDate - 30 * 24 * 60 * 60 * 1000;
break;
case CUSTOM_RANGE:
if (!customStartDate || !customEndDate) {
throw new Error('Start date and end date must be provided for CUSTOM_RANGE.');
}
startDate = customStartDate;
endDate = customEndDate;
break;
case LAST_24_HOURS:
default:
startDate = endDate - 24 * 60 * 60 * 1000;
}
return {
startDate,
endDate
};
}

View file

@ -6,46 +6,59 @@ import Search from '@/mstore/types/search';
import { getFilterFromJson } from 'Types/filter/newFilter';
interface Props {
onBeforeLoad?: () => Promise<any>;
appliedFilter: any;
onBeforeLoad?: () => Promise<void>;
appliedFilter: Record<string, any>;
loading: boolean;
}
const useSessionSearchQueryHandler = (props: Props) => {
const useSessionSearchQueryHandler = ({ onBeforeLoad, appliedFilter, loading }: Props) => {
const { searchStore } = useStore();
const [beforeHookLoaded, setBeforeHookLoaded] = useState(!props.onBeforeLoad);
const { appliedFilter, loading } = props;
const [beforeHookLoaded, setBeforeHookLoaded] = useState(!onBeforeLoad);
const history = useHistory();
// Apply filter from the query string when the component mounts
useEffect(() => {
const applyFilterFromQuery = async () => {
if (!loading && !searchStore.urlParsed) {
if (props.onBeforeLoad) {
await props.onBeforeLoad();
setBeforeHookLoaded(true);
}
try {
if (onBeforeLoad) {
await onBeforeLoad();
setBeforeHookLoaded(true);
}
const converter = JsonUrlConverter.urlParamsToJson(history.location.search);
const json: any = getFilterFromJson(converter.toJSON());
const filter = new Search(json);
searchStore.applyFilter(filter, true);
searchStore.setUrlParsed()
const converter = JsonUrlConverter.urlParamsToJson(history.location.search);
const json = getFilterFromJson(converter.toJSON());
const filter = new Search(json);
searchStore.applyFilter(filter, true);
searchStore.setUrlParsed();
} catch (error) {
console.error('Error applying filter from query:', error);
}
}
};
void applyFilterFromQuery();
}, [loading]);
}, [loading, onBeforeLoad, searchStore, history.location.search]);
// Update the URL whenever the appliedFilter changes
useEffect(() => {
const generateUrlQuery = () => {
const updateUrlWithFilter = () => {
if (!loading && beforeHookLoaded) {
const converter = JsonUrlConverter.jsonToUrlParams(appliedFilter);
history.replace({ search: converter });
const query = JsonUrlConverter.jsonToUrlParams(appliedFilter);
history.replace({ search: query });
}
};
generateUrlQuery();
}, [appliedFilter, loading, beforeHookLoaded]);
updateUrlWithFilter();
}, [appliedFilter, loading, beforeHookLoaded, history]);
// Ensure the URL syncs on remount if already parsed
useEffect(() => {
if (searchStore.urlParsed) {
const query = JsonUrlConverter.jsonToUrlParams(appliedFilter);
history.replace({ search: query });
}
}, [appliedFilter, searchStore.urlParsed, history]);
return null;
};

View file

@ -20,7 +20,7 @@ export default class FilterStore {
}
setTopValues = (key: string, values: TopValue[]) => {
this.topValues[key] = values.filter((value) => value !== null && value.value !== '');
this.topValues[key] = values?.filter((value) => value !== null && value.value !== '');
};
fetchTopValues = async (key: string, source?: string) => {

View file

@ -162,15 +162,14 @@ class SearchStore {
});
}
updateCurrentPage(page: number) {
updateCurrentPage(page: number, force = false) {
this.currentPage = page;
void this.fetchSessions();
void this.fetchSessions(force);
}
setActiveTab(tab: string) {
runInAction(() => {
this.activeTab = TAB_MAP[tab];
this.currentPage = 1;
});
}
@ -229,12 +228,13 @@ class SearchStore {
if (this.latestRequestTime) {
const period = Period({ rangeName: CUSTOM_RANGE, start: this.latestRequestTime, end: Date.now() });
const newTimestamps: any = period.toJSON();
filter.startTimestamp = newTimestamps.startDate;
filter.endTimestamp = newTimestamps.endDate;
filter.startDate = newTimestamps.startDate;
filter.endDate = newTimestamps.endDate;
}
searchService.checkLatestSessions(filter).then((response: any) => {
this.latestList = response;
this.latestRequestTime = Date.now();
runInAction(() => {
this.latestList = List(response);
});
});
}
@ -264,8 +264,10 @@ class SearchStore {
});
}
this.currentPage = 1;
if (filter.value && filter.value[0] && filter.value[0] !== '') {
this.fetchSessions();
void this.fetchSessions();
}
}
@ -336,6 +338,9 @@ class SearchStore {
filter.filters = filter.filters.concat(tagFilter);
}
this.latestRequestTime = Date.now();
this.latestList = List();
await sessionStore.fetchSessions({
...filter,
page: this.currentPage,

View file

@ -2,10 +2,10 @@ import { FilterCategory, FilterKey } from 'Types/filter/filterType';
import {
filtersMap,
generateFilterOptions,
liveFiltersMap,
liveFiltersMap
} from 'Types/filter/newFilter';
import { List } from 'immutable';
import { makeAutoObservable } from 'mobx';
import { makeAutoObservable, reaction } from 'mobx';
import Search from 'App/mstore/types/search';
import { checkFilterValue, IFilter } from 'App/mstore/types/filter';
import FilterItem from 'App/mstore/types/filterItem';
@ -63,11 +63,29 @@ class SearchStoreLive {
constructor() {
makeAutoObservable(this);
// Reset currentPage to 1 only on filter changes
reaction(
() => this.instance,
() => {
this.currentPage = 1;
void this.fetchSessions();
}
);
// Fetch sessions when currentPage changes
reaction(
() => this.currentPage,
() => {
void this.fetchSessions();
}
);
}
get filterList() {
return generateFilterOptions(filtersMap);
}
get filterListLive() {
return generateFilterOptions(liveFiltersMap);
}
@ -96,14 +114,12 @@ class SearchStoreLive {
edit(instance: Partial<Search>) {
this.instance = new Search(Object.assign({ ...this.instance }, instance));
this.currentPage = 1;
}
apply(filter: any, fromUrl: boolean) {
if (fromUrl) {
this.instance = new Search(filter);
this.currentPage = 1;
} else {
this.instance = { ...this.instance, ...filter };
}
@ -115,7 +131,6 @@ class SearchStoreLive {
updateCurrentPage(page: number) {
this.currentPage = page;
this.fetchSessions();
}
clearSearch() {
@ -140,22 +155,25 @@ class SearchStoreLive {
: null;
if (index > -1) {
const oldFilter = this.instance.filters[index];
const updatedFilter = {
...oldFilter,
value: oldFilter.value.concat(filter.value)
// Update existing filter
// @ts-ignore
this.instance.filters[index] = {
...this.instance.filters[index],
value: this.instance.filters[index].value.concat(filter.value)
};
oldFilter.merge(updatedFilter);
} else {
this.instance.filters.push(filter);
this.instance = new Search({
...this.instance.toData()
});
// Add new filter (create a new array reference to notify MobX)
this.instance.filters = [...this.instance.filters, filter];
}
if (filter.value && filter.value[0] && filter.value[0] !== '') {
this.fetchSessions();
}
// Update the instance to trigger reactions
this.instance = new Search({
...this.instance.toData()
});
// if (filter.value && filter.value[0] && filter.value[0] !== '') {
// void this.fetchSessions();
// }
}
addFilterByKeyAndValue(key: any, value: any, operator?: string, sourceOperator?: string, source?: string) {
@ -200,7 +218,7 @@ class SearchStoreLive {
};
async fetchSessions() {
await sessionStore.fetchLiveSessions(this.instance.toSearch());
await sessionStore.fetchLiveSessions({ ...this.instance.toSearch(), page: this.currentPage });
};
}

View file

@ -170,7 +170,7 @@ export default class SessionStore {
}
const nextEntryNum =
keys.length > 0
? Math.max(...keys.map((key) => this.prefetchedMobUrls[key].entryNum || 0)) + 1
? Math.max(...keys.map((key) => this.prefetchedMobUrls[key]?.entryNum || 0)) + 1
: 0;
this.prefetchedMobUrls[sessionId] = {
data: fileData,

View file

@ -1,7 +1,8 @@
import { DATE_RANGE_VALUES, CUSTOM_RANGE, getDateRangeFromValue } from 'App/dateRange';
import Filter, { checkFilterValue, IFilter } from 'App/mstore/types/filter';
import { CUSTOM_RANGE, DATE_RANGE_VALUES, getDateRangeFromValue } from 'App/dateRange';
import Filter, { IFilter } from 'App/mstore/types/filter';
import FilterItem from 'App/mstore/types/filterItem';
import { action, makeAutoObservable, observable } from 'mobx';
import { makeAutoObservable, observable } from 'mobx';
import { LAST_24_HOURS, LAST_30_DAYS, LAST_7_DAYS } from 'Types/app/period';
// @ts-ignore
const rangeValue = DATE_RANGE_VALUES.LAST_24_HOURS;
@ -69,7 +70,7 @@ export default class Search {
constructor(initialData?: Partial<ISearch>) {
makeAutoObservable(this, {
filters: observable,
filters: observable
});
Object.assign(this, {
name: '',
@ -142,11 +143,48 @@ export default class Search {
return new FilterItem(filter).toJson();
});
const { startDate, endDate } = this.getDateRange(js.rangeValue, js.startDate, js.endDate);
js.startDate = startDate;
js.endDate = endDate;
delete js.createdAt;
delete js.key;
return js;
}
private getDateRange(rangeName: string, customStartDate: number, customEndDate: number): {
startDate: number;
endDate: number
} {
let endDate = new Date().getTime();
let startDate: number;
switch (rangeName) {
case LAST_7_DAYS:
startDate = endDate - 7 * 24 * 60 * 60 * 1000;
break;
case LAST_30_DAYS:
startDate = endDate - 30 * 24 * 60 * 60 * 1000;
break;
case CUSTOM_RANGE:
if (!customStartDate || !customEndDate) {
throw new Error('Start date and end date must be provided for CUSTOM_RANGE.');
}
startDate = customStartDate;
endDate = customEndDate;
break;
case LAST_24_HOURS:
default:
startDate = endDate - 24 * 60 * 60 * 1000;
}
return {
startDate,
endDate
};
}
fromJS({ eventsOrder, filters, events, custom, ...filterData }: any) {
let startDate, endDate;
const rValue = filterData.rangeValue || rangeValue;
@ -176,3 +214,4 @@ export default class Search {
});
}
}

View file

@ -88,7 +88,6 @@ class UserStore {
get isEnterprise() {
return (
this.account?.edition === 'ee' ||
this.account?.edition === 'msaas' ||
this.authStore.authDetails?.edition === 'ee'
);
}

View file

@ -1,6 +1,8 @@
import Period, { CUSTOM_RANGE } from 'Types/app/period';
import { filtersMap } from 'Types/filter/newFilter';
import Period, { CUSTOM_RANGE, LAST_24_HOURS } from 'Types/app/period';
const DEFAULT_SORT = 'startTs';
const DEFAULT_ORDER = 'desc';
const DEFAULT_EVENTS_ORDER = 'then';
class Filter {
key: string;
@ -25,24 +27,28 @@ class Filter {
}
}
class InputJson {
export class InputJson {
filters: Filter[];
rangeValue: string;
startDate: number;
endDate: number;
startDate?: number;
endDate?: number;
sort: string;
order: string;
eventsOrder: string;
constructor(filters: Filter[], rangeValue: string, startDate: number, endDate: number, sort: string, order: string, eventsOrder: string) {
constructor(
filters: Filter[],
rangeValue: string,
sort: string,
order: string,
eventsOrder: string,
startDate?: string | number,
endDate?: string | number
) {
this.filters = filters;
// .map((f: any) => {
// const subFilters = f.filters ? f.filters.map((sf: any) => new Filter(sf.key, sf.operator, sf.value, sf.filters)) : undefined;
// return new Filter(f.key, f.operator, f.value, subFilters);
// });
this.rangeValue = rangeValue;
this.startDate = startDate;
this.endDate = endDate;
this.startDate = startDate ? +startDate : undefined;
this.endDate = endDate ? +endDate : undefined;
this.sort = sort;
this.order = order;
this.eventsOrder = eventsOrder;
@ -50,17 +56,28 @@ class InputJson {
toJSON() {
return {
filters: this.filters.map(f => f.toJSON()),
filters: this.filters.map((f) => f.toJSON()),
rangeValue: this.rangeValue,
startDate: this.startDate,
endDate: this.endDate,
startDate: this.startDate ?? null,
endDate: this.endDate ?? null,
sort: this.sort,
order: this.order,
eventsOrder: this.eventsOrder
};
}
}
fromJSON(json: Record<string, any>): InputJson {
return new InputJson(
json.filters.map((f: any) => new Filter(f.key, f.operator, f.value, f.filters)),
json.rangeValue,
json.sort,
json.order,
json.eventsOrder,
json.startDate,
json.endDate
);
}
}
export class JsonUrlConverter {
static keyMap = {
@ -76,35 +93,46 @@ export class JsonUrlConverter {
filters: 'f'
};
static getDateRangeValues(rangeValue: string, startDate: number | undefined, endDate: number | undefined): [number, number] {
if (rangeValue === 'CUSTOM_RANGE') {
return [startDate!, endDate!];
static getDateRangeValues(
rangeValue: string,
startDate: string | null,
endDate: string | null
): [string, string] {
if (rangeValue === CUSTOM_RANGE) {
return [startDate || '', endDate || ''];
}
const period = Period({ rangeName: rangeValue });
const period: any = Period({ rangeName: rangeValue });
return [period.start, period.end];
}
static jsonToUrlParams(json: InputJson): string {
static jsonToUrlParams(json: Record<string, any>): string {
const params = new URLSearchParams();
const addFilterParams = (filter: Filter, prefix: string) => {
params.append(`${prefix}${this.keyMap.key}`, filter.key);
params.append(`${prefix}${this.keyMap.operator}`, filter.operator);
if (filter.value) {
filter.value.forEach((v, i) => params.append(`${prefix}${this.keyMap.value}[${i}]`, v || ''));
}
if (filter.filters) {
filter.filters.forEach((f, i) => addFilterParams(f, `${prefix}${this.keyMap.filters}[${i}].`));
}
filter.value?.forEach((v, i) =>
params.append(`${prefix}${this.keyMap.value}[${i}]`, v || '')
);
filter.filters?.forEach((f, i) =>
addFilterParams(f, `${prefix}${this.keyMap.filters}[${i}].`)
);
};
json.filters.forEach((filter, index) => addFilterParams(filter, `${this.keyMap.filters}[${index}].`));
const rangeValues = this.getDateRangeValues(json.rangeValue, json.startDate, json.endDate);
json.filters.forEach((filter: any, index: number) =>
addFilterParams(filter, `${this.keyMap.filters}[${index}].`)
);
params.append(this.keyMap.rangeValue, json.rangeValue);
params.append(this.keyMap.startDate, rangeValues[0].toString());
params.append(this.keyMap.endDate, rangeValues[1].toString());
if (json.rangeValue === CUSTOM_RANGE) {
const rangeValues = this.getDateRangeValues(
json.rangeValue,
json.startDate?.toString() || null,
json.endDate?.toString() || null
);
params.append(this.keyMap.startDate, rangeValues[0]);
params.append(this.keyMap.endDate, rangeValues[1]);
}
params.append(this.keyMap.sort, json.sort);
params.append(this.keyMap.order, json.order);
params.append(this.keyMap.eventsOrder, json.eventsOrder);
@ -130,7 +158,7 @@ export class JsonUrlConverter {
filters.push(getFilterParams(`${prefix}${this.keyMap.filters}[${index}].`));
index++;
}
return new Filter(key, operator, value.length ? value : '', filters.length ? filters : []);
return new Filter(key, operator, value.length ? value : [], filters.length ? filters : []);
};
const filters: Filter[] = [];
@ -140,23 +168,22 @@ export class JsonUrlConverter {
index++;
}
const rangeValue = params.get(this.keyMap.rangeValue) || 'LAST_24_HOURS';
const rangeValue = params.get(this.keyMap.rangeValue) || LAST_24_HOURS;
const rangeValues = this.getDateRangeValues(rangeValue, params.get(this.keyMap.startDate), params.get(this.keyMap.endDate));
const startDate = rangeValues[0];
const endDate = rangeValues[1];
return new InputJson(
filters,
rangeValue,
startDate,
endDate,
params.get(this.keyMap.sort) || 'startTs',
params.get(this.keyMap.order) || 'desc',
params.get(this.keyMap.eventsOrder) || 'then'
params.get(this.keyMap.sort) || DEFAULT_SORT,
params.get(this.keyMap.order) || DEFAULT_ORDER,
params.get(this.keyMap.eventsOrder) || DEFAULT_EVENTS_ORDER,
rangeValues[0],
rangeValues[1]
);
}
}
// Example usage
// const urlParams = '?f[0].k=click&f[0].op=on&f[0].v[0]=Refresh&f[1].k=fetch&f[1].op=is&f[1].v[0]=&f[1].f[0].k=fetchUrl&f[1].f[0].op=is&f[1].f[0].v[0]=/g/collect&f[1].f[1].k=fetchStatusCode&f[1].f[1].op=>=&f[1].f[1].v[0]=400&f[1].f[2].k=fetchMethod&f[1].f[2].op=is&f[1].f[2].v[0]=&f[1].f[3].k=fetchDuration&f[1].f[3].op==&f[1].f[3].v[0]=&f[1].f[4].k=fetchRequestBody&f[1].f[4].op=is&f[1].f[4].v[0]=&f[1].f[5].k=fetchResponseBody&f[1].f[5].op=is&f[1].f[5].v[0]=&rv=LAST_24_HOURS&sd=1731343412555&ed=1731429812555&s=startTs&o=desc&st=false&eo=then';
// const parsedJson = JsonUrlConverter.urlParamsToJson(urlParams);

View file

@ -5,7 +5,7 @@ export function validateIP(value) {
export function validateURL(value) {
if (typeof value !== 'string') return false;
const urlRegex = /^(http|https):\/\/(?:www\.)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}(\/\S*)?$/i;
const urlRegex = /^(http|https):\/\/(?:www\.)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}(:\d+)?(\/\S*)?$/i;
const ipRegex = /^(http|https):\/\/(?:localhost|(\d{1,3}\.){3}\d{1,3})(:\d+)?(\/\S*)?$/i;
return urlRegex.test(value) || ipRegex.test(value);
}
@ -89,4 +89,4 @@ export const validatePassword = (password) => {
const regex =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?])[A-Za-z\d!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]{8,}$/;
return regex.test(password);
};
};

View file

@ -26,7 +26,7 @@
"@babel/plugin-transform-private-methods": "^7.23.3",
"@floating-ui/react-dom-interactions": "^0.10.3",
"@medv/finder": "^3.1.0",
"@sentry/browser": "^5.21.1",
"@sentry/browser": "^8.34.0",
"@svg-maps/world": "^1.0.1",
"@tanstack/react-query": "^5.56.2",
"@wojtekmaj/react-daterange-picker": "^6.0.0",

View file

@ -9,7 +9,7 @@ COMMON_PG_PASSWORD="change_me_pg_password"
COMMON_VERSION="v1.16.0"
## DB versions
######################################
POSTGRES_VERSION="14.5.0"
POSTGRES_VERSION="15.10.0"
REDIS_VERSION="6.0.12-debian-10-r33"
MINIO_VERSION="2023.2.10-debian-11-r1"
######################################

View file

@ -15,7 +15,7 @@ services:
image: bitnami/redis:${REDIS_VERSION}
container_name: redis
volumes:
- redisdata:/var/lib/postgresql/data
- redisdata:/bitnami/redis/data
networks:
- openreplay-net
environment:

View file

@ -18,4 +18,4 @@ version: 0.1.1
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
AppVersion: "v1.21.0"
AppVersion: "v1.21.1"

View file

@ -18,4 +18,4 @@ version: 0.1.7
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
AppVersion: "v1.21.0"
AppVersion: "v1.21.6"

View file

@ -18,4 +18,4 @@ version: 0.1.10
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
AppVersion: "v1.21.0"
AppVersion: "v1.21.10"