mirror of
https://github.com/PostHog/posthog.git
synced 2024-11-21 13:39:22 +01:00
Destroy lodash (#1864)
* Convert utils to TS and add missing lodash-like functions * Purge lodash, using utils and ES features instead * Remove lodash as a dependency * Fix Annotation.created_at default value (was null) * Convert all of utils to TypeScript * Update ESLint rule @typescript-eslint/explicit-module-boundary-types * Put all @types/* into devDependencies * Lower @typescript-eslint/explicit-function-return-type severity * Fix Annotation.created_at in a better way * Don't copy item on push in groupBy * Use `Set.has()` instead of `in Set` * Update .eslintrc.js * Update .eslintrc.js
This commit is contained in:
parent
ff1fb54eb5
commit
c3d3f83c49
@ -56,7 +56,12 @@ module.exports = {
|
||||
allowExpressions: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/explicit-module-boundary-types': ['error'],
|
||||
'@typescript-eslint/explicit-module-boundary-types': [
|
||||
'error',
|
||||
{
|
||||
allowArgumentsExplicitlyTypedAsAny: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -4,7 +4,6 @@ import { userLogic } from 'scenes/userLogic'
|
||||
import { Button, Popover, Row, Input, Checkbox, Tooltip } from 'antd'
|
||||
import { humanFriendlyDetailedTime } from '~/lib/utils'
|
||||
import { DeleteOutlined, PlusOutlined, GlobalOutlined, CloseOutlined } from '@ant-design/icons'
|
||||
import _ from 'lodash'
|
||||
import { annotationsLogic } from './annotationsLogic'
|
||||
import moment from 'moment'
|
||||
import { useEscapeKey } from 'lib/hooks/useEscapeKey'
|
||||
@ -138,37 +137,41 @@ export function AnnotationMarker({
|
||||
</div>
|
||||
) : (
|
||||
<div ref={popupRef} style={{ minWidth: 300 }}>
|
||||
{_.orderBy(annotations, ['created_at'], ['asc']).map((data) => (
|
||||
<div key={data.id} style={{ marginBottom: 25 }}>
|
||||
<Row justify="space-between" align="middle">
|
||||
<div>
|
||||
<b style={{ marginRight: 5 }}>
|
||||
{data.created_by === 'local'
|
||||
? name || email
|
||||
: data.created_by &&
|
||||
(data.created_by.first_name || data.created_by.email)}
|
||||
</b>
|
||||
<i style={{ color: 'gray', marginRight: 6 }}>
|
||||
{humanFriendlyDetailedTime(data.created_at)}
|
||||
</i>
|
||||
{data.scope !== 'dashboard_item' && (
|
||||
<Tooltip title="This note is shown on all charts">
|
||||
<GlobalOutlined></GlobalOutlined>
|
||||
</Tooltip>
|
||||
{[...annotations]
|
||||
.sort((annotationA, annotationB) => annotationA.created_at - annotationB.created_at)
|
||||
.map((data) => (
|
||||
<div key={data.id} style={{ marginBottom: 25 }}>
|
||||
<Row justify="space-between" align="middle">
|
||||
<div>
|
||||
<b style={{ marginRight: 5 }}>
|
||||
{data.created_by === 'local'
|
||||
? name || email
|
||||
: data.created_by &&
|
||||
(data.created_by.first_name || data.created_by.email)}
|
||||
</b>
|
||||
<i style={{ color: 'gray', marginRight: 6 }}>
|
||||
{humanFriendlyDetailedTime(data.created_at)}
|
||||
</i>
|
||||
{data.scope !== 'dashboard_item' && (
|
||||
<Tooltip title="This note is shown on all charts">
|
||||
<GlobalOutlined></GlobalOutlined>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
{(!data.created_by ||
|
||||
data.created_by.id === id ||
|
||||
data.created_by === 'local') && (
|
||||
<DeleteOutlined
|
||||
className="button-border clickable"
|
||||
onClick={() => {
|
||||
onDelete(data)
|
||||
}}
|
||||
></DeleteOutlined>
|
||||
)}
|
||||
</div>
|
||||
{(!data.created_by || data.created_by.id === id || data.created_by === 'local') && (
|
||||
<DeleteOutlined
|
||||
className="button-border clickable"
|
||||
onClick={() => {
|
||||
onDelete(data)
|
||||
}}
|
||||
></DeleteOutlined>
|
||||
)}
|
||||
</Row>
|
||||
<span>{data.content}</span>
|
||||
</div>
|
||||
))}
|
||||
</Row>
|
||||
<span>{data.content}</span>
|
||||
</div>
|
||||
))}
|
||||
{textAreaVisible && (
|
||||
<TextArea
|
||||
maxLength={300}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { kea } from 'kea'
|
||||
import api from 'lib/api'
|
||||
import moment from 'moment'
|
||||
import _ from 'lodash'
|
||||
import { determineDifferenceType, deleteWithUndo, toParams } from '~/lib/utils'
|
||||
import { determineDifferenceType, deleteWithUndo, toParams, groupBy } from '~/lib/utils'
|
||||
import { annotationsModel } from '~/models/annotationsModel'
|
||||
import { getNextKey } from './utils'
|
||||
|
||||
@ -114,12 +113,8 @@ export const annotationsLogic = kea({
|
||||
],
|
||||
groupedAnnotations: [
|
||||
() => [selectors.annotationsList, selectors.diffType],
|
||||
(annotationsList, diffType) => {
|
||||
const groupedResults = _.groupBy(annotationsList, (annote) =>
|
||||
moment(annote['date_marker']).startOf(diffType)
|
||||
)
|
||||
return groupedResults
|
||||
},
|
||||
(annotationsList, diffType) =>
|
||||
groupBy(annotationsList, (annotation) => moment(annotation['date_marker']).startOf(diffType)),
|
||||
],
|
||||
}),
|
||||
listeners: ({ actions, props }) => ({
|
||||
|
@ -4,7 +4,6 @@ import { commandPaletteLogicType } from 'types/lib/components/CommandPalette/com
|
||||
import Fuse from 'fuse.js'
|
||||
import { dashboardsModel } from '~/models/dashboardsModel'
|
||||
import { Parser } from 'expr-eval'
|
||||
import _ from 'lodash'
|
||||
import {
|
||||
CommentOutlined,
|
||||
FundOutlined,
|
||||
@ -37,7 +36,7 @@ import {
|
||||
import { DashboardType } from '~/types'
|
||||
import api from 'lib/api'
|
||||
import { appUrlsLogic } from '../AppEditorLink/appUrlsLogic'
|
||||
import { copyToClipboard, isURL } from 'lib/utils'
|
||||
import { copyToClipboard, isURL, sample, uniqueBy } from 'lib/utils'
|
||||
import { personalAPIKeysLogic } from '../PersonalAPIKeys/personalAPIKeysLogic'
|
||||
|
||||
// If CommandExecutor returns CommandFlow, flow will be entered
|
||||
@ -306,8 +305,8 @@ export const commandPaletteLogic = kea<
|
||||
if (result.guarantee) guaranteedResults.push(result)
|
||||
else fusableResults.push(result)
|
||||
}
|
||||
fusableResults = _.uniqBy(fusableResults, 'display')
|
||||
guaranteedResults = _.uniqBy(guaranteedResults, 'display')
|
||||
fusableResults = uniqueBy(fusableResults, (result) => result.display)
|
||||
guaranteedResults = uniqueBy(guaranteedResults, (result) => result.display)
|
||||
const fusedResults = argument
|
||||
? new Fuse(fusableResults, {
|
||||
keys: ['display', 'synonyms'],
|
||||
@ -315,7 +314,7 @@ export const commandPaletteLogic = kea<
|
||||
.search(argument)
|
||||
.slice(0, RESULTS_MAX)
|
||||
.map((result) => result.item)
|
||||
: _.sampleSize(fusableResults, RESULTS_MAX - guaranteedResults.length)
|
||||
: sample(fusableResults, RESULTS_MAX - guaranteedResults.length)
|
||||
const finalResults = guaranteedResults.concat(fusedResults)
|
||||
// put global scope last
|
||||
return finalResults.sort((resultA, resultB) =>
|
||||
|
@ -7,7 +7,6 @@ import { cohortsModel } from '../../../models/cohortsModel'
|
||||
import { keyMapping } from 'lib/components/PropertyKeyInfo'
|
||||
import { Popover, Row } from 'antd'
|
||||
import { CloseButton, formatPropertyLabel } from 'lib/utils'
|
||||
import _ from 'lodash'
|
||||
import '../../../scenes/actions/Actions.scss'
|
||||
|
||||
const FilterRow = React.memo(function FilterRow({
|
||||
@ -53,7 +52,7 @@ const FilterRow = React.memo(function FilterRow({
|
||||
</Button>
|
||||
)}
|
||||
</Popover>
|
||||
{!_.isEmpty(filters[index]) && (
|
||||
{Object.keys(filters[index]).length && (
|
||||
<CloseButton
|
||||
className="ml-1"
|
||||
onClick={() => {
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React from 'react'
|
||||
import React, { CSSProperties, PropsWithChildren } from 'react'
|
||||
import api from './api'
|
||||
import { toast } from 'react-toastify'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Spin } from 'antd'
|
||||
import moment from 'moment'
|
||||
import { EventType } from '~/types'
|
||||
|
||||
const SI_PREFIXES = [
|
||||
const SI_PREFIXES: { value: number; symbol: string }[] = [
|
||||
{ value: 1e18, symbol: 'E' },
|
||||
{ value: 1e15, symbol: 'P' },
|
||||
{ value: 1e12, symbol: 'T' },
|
||||
@ -16,14 +16,14 @@ const SI_PREFIXES = [
|
||||
]
|
||||
const TRAILING_ZERO_REGEX = /\.0+$|(\.[0-9]*[1-9])0+$/
|
||||
|
||||
export function uuid() {
|
||||
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
|
||||
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
|
||||
export function uuid(): string {
|
||||
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) =>
|
||||
(parseInt(c) ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (parseInt(c) / 4)))).toString(16)
|
||||
)
|
||||
}
|
||||
|
||||
export let toParams = (obj) => {
|
||||
let handleVal = (val) => {
|
||||
export function toParams(obj: Record<string, any>): string {
|
||||
function handleVal(val: any): string {
|
||||
if (val._isAMomentObject) return encodeURIComponent(val.format('YYYY-MM-DD'))
|
||||
val = typeof val === 'object' ? JSON.stringify(val) : val
|
||||
return encodeURIComponent(val)
|
||||
@ -33,9 +33,11 @@ export let toParams = (obj) => {
|
||||
.map(([key, val]) => `${key}=${handleVal(val)}`)
|
||||
.join('&')
|
||||
}
|
||||
export let fromParams = () =>
|
||||
window.location.search != ''
|
||||
? window.location.search
|
||||
|
||||
export function fromParams(): Record<string, any> {
|
||||
return window.location.search === ''
|
||||
? {}
|
||||
: window.location.search
|
||||
.slice(1)
|
||||
.split('&')
|
||||
.reduce((a, b) => {
|
||||
@ -43,39 +45,52 @@ export let fromParams = () =>
|
||||
a[b[0]] = decodeURIComponent(b[1])
|
||||
return a
|
||||
}, {})
|
||||
: {}
|
||||
}
|
||||
|
||||
export let colors = ['success', 'secondary', 'warning', 'primary', 'danger', 'info', 'dark', 'light']
|
||||
export let percentage = (division) =>
|
||||
division
|
||||
export const colors = ['success', 'secondary', 'warning', 'primary', 'danger', 'info', 'dark', 'light']
|
||||
|
||||
export function percentage(division: number): string {
|
||||
return division
|
||||
? division.toLocaleString(undefined, {
|
||||
style: 'percent',
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
: ''
|
||||
}
|
||||
|
||||
export let Loading = () => (
|
||||
<div className="loading-overlay">
|
||||
<div></div>
|
||||
<Spin />
|
||||
</div>
|
||||
)
|
||||
|
||||
export const TableRowLoading = ({ colSpan = 1, asOverlay = false }) => (
|
||||
<tr className={asOverlay ? 'loading-overlay over-table' : ''}>
|
||||
<td colSpan={colSpan} style={{ padding: 50, textAlign: 'center' }}>
|
||||
export function Loading(): JSX.Element {
|
||||
return (
|
||||
<div className="loading-overlay">
|
||||
<Spin />
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SceneLoading = () => (
|
||||
<div style={{ textAlign: 'center', marginTop: '20vh' }}>
|
||||
<Spin />
|
||||
</div>
|
||||
)
|
||||
export function TableRowLoading({
|
||||
colSpan = 1,
|
||||
asOverlay = false,
|
||||
}: {
|
||||
colSpan: number
|
||||
asOverlay: boolean
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<tr className={asOverlay ? 'loading-overlay over-table' : ''}>
|
||||
<td colSpan={colSpan} style={{ padding: 50, textAlign: 'center' }}>
|
||||
<Spin />
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
export let CloseButton = (props) => {
|
||||
export function SceneLoading(): JSX.Element {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', marginTop: '20vh' }}>
|
||||
<Spin />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function CloseButton(props: Record<string, any>): JSX.Element {
|
||||
return (
|
||||
<span {...props} className={'close cursor-pointer ' + props.className} style={{ ...props.style }}>
|
||||
<span aria-hidden="true">×</span>
|
||||
@ -83,7 +98,7 @@ export let CloseButton = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
export function Card(props) {
|
||||
export function Card(props: Record<string, any>): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
@ -97,13 +112,13 @@ export function Card(props) {
|
||||
)
|
||||
}
|
||||
|
||||
export const deleteWithUndo = ({ undo = false, ...props }) => {
|
||||
export function deleteWithUndo({ undo = false, ...props }: Record<string, any>): void {
|
||||
api.update('api/' + props.endpoint + '/' + props.object.id, {
|
||||
...props.object,
|
||||
deleted: !undo,
|
||||
}).then(() => {
|
||||
props.callback?.()
|
||||
let response = (
|
||||
const response = (
|
||||
<span>
|
||||
<b>{props.object.name ?? 'Untitled'}</b>
|
||||
{!undo ? ' deleted. Click here to undo.' : ' deletion undone.'}
|
||||
@ -118,7 +133,17 @@ export const deleteWithUndo = ({ undo = false, ...props }) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const DeleteWithUndo = (props) => {
|
||||
export function DeleteWithUndo(
|
||||
props: PropsWithChildren<{
|
||||
endpoint: string
|
||||
object: {
|
||||
name: string
|
||||
id: number
|
||||
}
|
||||
className: string
|
||||
style: CSSProperties
|
||||
}>
|
||||
): JSX.Element {
|
||||
const { className, style, children } = props
|
||||
return (
|
||||
<a
|
||||
@ -134,17 +159,8 @@ export const DeleteWithUndo = (props) => {
|
||||
</a>
|
||||
)
|
||||
}
|
||||
DeleteWithUndo.propTypes = {
|
||||
endpoint: PropTypes.string.isRequired,
|
||||
object: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
id: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
className: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
}
|
||||
|
||||
export let selectStyle = {
|
||||
export const selectStyle: Record<string, (base: Partial<CSSProperties>) => Partial<CSSProperties>> = {
|
||||
control: (base) => ({
|
||||
...base,
|
||||
height: 31,
|
||||
@ -172,27 +188,26 @@ export let selectStyle = {
|
||||
}),
|
||||
}
|
||||
|
||||
export let debounce = (func, wait, immediate) => {
|
||||
var timeout
|
||||
export function debounce(func: (...args: any) => void, wait: number, immediate: boolean, ...args: any): () => void {
|
||||
let timeout: number | undefined
|
||||
return function () {
|
||||
var context = this, // eslint-disable-line
|
||||
args = arguments
|
||||
var later = function () {
|
||||
timeout = null
|
||||
const context = this // eslint-disable-line
|
||||
function later(): void {
|
||||
timeout = undefined
|
||||
if (!immediate) func.apply(context, args)
|
||||
}
|
||||
var callNow = immediate && !timeout
|
||||
const callNow = immediate && !timeout
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(later, wait)
|
||||
if (callNow) func.apply(context, args)
|
||||
}
|
||||
}
|
||||
|
||||
export const capitalizeFirstLetter = (string) => {
|
||||
export function capitalizeFirstLetter(string: string): string {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
}
|
||||
|
||||
export const operatorMap = {
|
||||
export const operatorMap: Record<string, string> = {
|
||||
exact: '= equals',
|
||||
is_not: "≠ doesn't equal",
|
||||
icontains: '∋ contains',
|
||||
@ -205,12 +220,16 @@ export const operatorMap = {
|
||||
is_not_set: '✕ is not set',
|
||||
}
|
||||
|
||||
export function isOperatorFlag(operator) {
|
||||
export function isOperatorFlag(operator: string): boolean {
|
||||
// these filter operators can only be just set, no additional parameter
|
||||
return ['is_set', 'is_not_set'].includes(operator)
|
||||
}
|
||||
|
||||
export function formatPropertyLabel(item, cohorts, keyMapping) {
|
||||
export function formatPropertyLabel(
|
||||
item: Record<string, any>,
|
||||
cohorts: Record<string, any>[],
|
||||
keyMapping: Record<string, Record<string, any>>
|
||||
): string {
|
||||
const { value, key, operator, type } = item
|
||||
return type === 'cohort'
|
||||
? cohorts?.find((cohort) => cohort.id === value)?.name || value
|
||||
@ -220,88 +239,92 @@ export function formatPropertyLabel(item, cohorts, keyMapping) {
|
||||
: ` ${(operatorMap[operator || 'exact'] || '?').split(' ')[0]} ${value || ''}`)
|
||||
}
|
||||
|
||||
export const formatProperty = (property) => {
|
||||
export function formatProperty(property: Record<string, any>): string {
|
||||
return property.key + ` ${operatorMap[property.operator || 'exact'].split(' ')[0]} ` + property.value
|
||||
}
|
||||
|
||||
// Format a label that gets returned from the /insights api
|
||||
export const formatLabel = (label, action) => {
|
||||
let math = 'Total'
|
||||
export function formatLabel(label: string, action: Record<string, any>): string {
|
||||
const math = 'Total'
|
||||
if (action.math === 'dau') label += ` (${action.math.toUpperCase()}) `
|
||||
else label += ` (${math}) `
|
||||
if (action && action.properties && !_.isEmpty(action.properties)) {
|
||||
if (action?.properties?.length) {
|
||||
label += ` (${action.properties
|
||||
.map((property) => operatorMap[property.operator || 'exact'].split(' ')[0] + ' ' + property.value)
|
||||
.join(', ')})`
|
||||
}
|
||||
|
||||
return label
|
||||
}
|
||||
|
||||
export const deletePersonData = (person, callback) => {
|
||||
window.confirm('Are you sure you want to delete this user? This cannot be undone') &&
|
||||
export function deletePersonData(person: Record<string, any>, callback: () => void): void {
|
||||
if (window.confirm('Are you sure you want to delete this user? This cannot be undone')) {
|
||||
api.delete('api/person/' + person.id).then(() => {
|
||||
toast('Person succesfully deleted.')
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const savePersonData = (person) => {
|
||||
export function savePersonData(person: Record<string, any>): void {
|
||||
api.update('api/person/' + person.id, person).then(() => {
|
||||
toast('Person Updated')
|
||||
})
|
||||
}
|
||||
|
||||
export const objectsEqual = (obj1, obj2) => JSON.stringify(obj1) === JSON.stringify(obj2)
|
||||
export function objectsEqual(obj1: any, obj2: any): boolean {
|
||||
return JSON.stringify(obj1) === JSON.stringify(obj2)
|
||||
}
|
||||
|
||||
export const idToKey = (array, keyField = 'id') => {
|
||||
const object = {}
|
||||
export function idToKey(array: Record<string, any>[], keyField: string = 'id'): any {
|
||||
const object: Record<string, any> = {}
|
||||
for (const element of array) {
|
||||
object[element[keyField]] = element
|
||||
}
|
||||
return object
|
||||
}
|
||||
|
||||
export const delay = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms))
|
||||
export function delay(ms: number): Promise<number> {
|
||||
return new Promise((resolve) => window.setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
// Trigger a window.reisize event a few times 0...2 sec after the menu was collapsed/expanded
|
||||
// We need this so the dashboard resizes itself properly, as the available div width will still
|
||||
// change when the sidebar's expansion is animating.
|
||||
export const triggerResize = () => {
|
||||
export function triggerResize(): void {
|
||||
try {
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
} catch (error) {
|
||||
// will break on IE11
|
||||
}
|
||||
}
|
||||
export const triggerResizeAfterADelay = () => {
|
||||
export function triggerResizeAfterADelay(): void {
|
||||
for (const delay of [10, 100, 500, 750, 1000, 2000]) {
|
||||
window.setTimeout(triggerResize, delay)
|
||||
}
|
||||
}
|
||||
|
||||
export function clearDOMTextSelection() {
|
||||
export function clearDOMTextSelection(): void {
|
||||
if (window.getSelection) {
|
||||
if (window.getSelection().empty) {
|
||||
if (window.getSelection()?.empty) {
|
||||
// Chrome
|
||||
window.getSelection().empty()
|
||||
} else if (window.getSelection().removeAllRanges) {
|
||||
window.getSelection()?.empty()
|
||||
} else if (window.getSelection()?.removeAllRanges) {
|
||||
// Firefox
|
||||
window.getSelection().removeAllRanges()
|
||||
window.getSelection()?.removeAllRanges()
|
||||
}
|
||||
} else if (document.selection) {
|
||||
} else if ((document as any).selection) {
|
||||
// IE?
|
||||
document.selection.empty()
|
||||
;(document as any).selection.empty()
|
||||
}
|
||||
}
|
||||
|
||||
export const posthogEvents = ['$autocapture', '$pageview', '$identify', '$pageleave']
|
||||
|
||||
export function isAndroidOrIOS() {
|
||||
export function isAndroidOrIOS(): boolean {
|
||||
return typeof window !== 'undefined' && /Android|iPhone|iPad|iPod/i.test(window.navigator.userAgent)
|
||||
}
|
||||
|
||||
export function slugify(text) {
|
||||
export function slugify(text: string): string {
|
||||
return text
|
||||
.toString() // Cast to string
|
||||
.toLowerCase() // Convert the string to lowercase letters
|
||||
@ -312,26 +335,26 @@ export function slugify(text) {
|
||||
.replace(/--+/g, '-')
|
||||
}
|
||||
|
||||
export function humanFriendlyDuration(d) {
|
||||
export function humanFriendlyDuration(d: string | number): string {
|
||||
d = Number(d)
|
||||
var days = Math.floor(d / 86400)
|
||||
var h = Math.floor((d % 86400) / 3600)
|
||||
var m = Math.floor((d % 3600) / 60)
|
||||
var s = Math.floor((d % 3600) % 60)
|
||||
const days = Math.floor(d / 86400)
|
||||
const h = Math.floor((d % 86400) / 3600)
|
||||
const m = Math.floor((d % 3600) / 60)
|
||||
const s = Math.floor((d % 3600) % 60)
|
||||
|
||||
var dayDisplay = days > 0 ? days + 'd ' : ''
|
||||
var hDisplay = h > 0 ? h + (h == 1 ? 'hr ' : 'hrs ') : ''
|
||||
var mDisplay = m > 0 ? m + (m == 1 ? 'min ' : 'mins ') : ''
|
||||
var sDisplay = s > 0 ? s + 's' : hDisplay || mDisplay ? '' : '0s'
|
||||
const dayDisplay = days > 0 ? days + 'd ' : ''
|
||||
const hDisplay = h > 0 ? h + (h == 1 ? 'hr ' : 'hrs ') : ''
|
||||
const mDisplay = m > 0 ? m + (m == 1 ? 'min ' : 'mins ') : ''
|
||||
const sDisplay = s > 0 ? s + 's' : hDisplay || mDisplay ? '' : '0s'
|
||||
return days > 0 ? dayDisplay + hDisplay : hDisplay + mDisplay + sDisplay
|
||||
}
|
||||
|
||||
export function humanFriendlyDiff(from, to) {
|
||||
export function humanFriendlyDiff(from: moment.MomentInput, to: moment.MomentInput): string {
|
||||
const diff = moment(to).diff(moment(from), 'seconds')
|
||||
return humanFriendlyDuration(diff)
|
||||
}
|
||||
|
||||
export function humanFriendlyDetailedTime(date, withSeconds = false) {
|
||||
export function humanFriendlyDetailedTime(date: moment.MomentInput, withSeconds: boolean = false): string {
|
||||
let formatString = 'MMMM Do YYYY h:mm'
|
||||
const today = moment().startOf('day')
|
||||
const yesterday = today.clone().subtract(1, 'days').startOf('day')
|
||||
@ -345,21 +368,21 @@ export function humanFriendlyDetailedTime(date, withSeconds = false) {
|
||||
return moment(date).format(formatString)
|
||||
}
|
||||
|
||||
export function stripHTTP(url) {
|
||||
export function stripHTTP(url: string): string {
|
||||
url = url.replace(/(^[0-9]+_)/, '')
|
||||
url = url.replace(/(^\w+:|^)\/\//, '')
|
||||
return url
|
||||
}
|
||||
|
||||
export function isURL(string) {
|
||||
export function isURL(string: string): boolean {
|
||||
if (!string) return false
|
||||
// https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url
|
||||
var expression = /^\s*https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi
|
||||
var regex = new RegExp(expression)
|
||||
return string.match && string.match(regex)
|
||||
const expression = /^\s*https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi
|
||||
const regex = new RegExp(expression)
|
||||
return !!string.match(regex)
|
||||
}
|
||||
|
||||
export const eventToName = (event) => {
|
||||
export function eventToName(event: EventType): string {
|
||||
if (event.event !== '$autocapture') return event.event
|
||||
let name = ''
|
||||
if (event.properties.$event_type === 'click') name += 'clicked '
|
||||
@ -379,7 +402,10 @@ export const eventToName = (event) => {
|
||||
return name
|
||||
}
|
||||
|
||||
export function determineDifferenceType(firstDate, secondDate) {
|
||||
export function determineDifferenceType(
|
||||
firstDate: moment.MomentInput,
|
||||
secondDate: moment.MomentInput
|
||||
): 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' {
|
||||
const first = moment(firstDate)
|
||||
const second = moment(secondDate)
|
||||
if (first.diff(second, 'years') !== 0) return 'year'
|
||||
@ -390,7 +416,7 @@ export function determineDifferenceType(firstDate, secondDate) {
|
||||
else return 'minute'
|
||||
}
|
||||
|
||||
export const dateMapping = {
|
||||
export const dateMapping: Record<string, string[]> = {
|
||||
Today: ['dStart'],
|
||||
Yesterday: ['-1d', 'dStart'],
|
||||
'Last 24 hours': ['-24h'],
|
||||
@ -407,18 +433,21 @@ export const dateMapping = {
|
||||
|
||||
export const isDate = /([0-9]{4}-[0-9]{2}-[0-9]{2})/
|
||||
|
||||
export function dateFilterToText(date_from, date_to) {
|
||||
if (isDate.test(date_from)) return `${date_from} - ${date_to}`
|
||||
if (moment.isMoment(date_from)) return `${date_from.format('YYYY-MM-DD')} - ${date_to.format('YYYY-MM-DD')}`
|
||||
if (date_from === 'dStart') return 'Today' // Changed to "last 24 hours" but this is backwards compatibility
|
||||
export function dateFilterToText(dateFrom: string | moment.Moment, dateTo: string | moment.Moment): string {
|
||||
if (moment.isMoment(dateFrom) && moment.isMoment(dateTo))
|
||||
return `${dateFrom.format('YYYY-MM-DD')} - ${dateTo.format('YYYY-MM-DD')}`
|
||||
dateFrom = dateFrom as string
|
||||
dateTo = dateTo as string
|
||||
if (isDate.test(dateFrom) && isDate.test(dateTo)) return `${dateFrom} - ${dateTo}`
|
||||
if (dateFrom === 'dStart') return 'Today' // Changed to "last 24 hours" but this is backwards compatibility
|
||||
let name = 'Last 7 days'
|
||||
Object.entries(dateMapping).map(([key, value]) => {
|
||||
if (value[0] === date_from && value[1] === date_to) name = key
|
||||
if (value[0] === dateFrom && value[1] === dateTo) name = key
|
||||
})[0]
|
||||
return name
|
||||
}
|
||||
|
||||
export function humanizeNumber(number, digits = 1) {
|
||||
export function humanizeNumber(number: number, digits: number = 1): string {
|
||||
// adapted from https://stackoverflow.com/a/9462382/624476
|
||||
let matchingPrefix = SI_PREFIXES[SI_PREFIXES.length - 1]
|
||||
for (const currentPrefix of SI_PREFIXES) {
|
||||
@ -430,7 +459,7 @@ export function humanizeNumber(number, digits = 1) {
|
||||
return (number / matchingPrefix.value).toFixed(digits).replace(TRAILING_ZERO_REGEX, '$1') + matchingPrefix.symbol
|
||||
}
|
||||
|
||||
export function copyToClipboard(value, description) {
|
||||
export function copyToClipboard(value: string, description?: string): boolean {
|
||||
const descriptionAdjusted = description ? description.trim() + ' ' : ''
|
||||
try {
|
||||
navigator.clipboard.writeText(value)
|
||||
@ -442,18 +471,54 @@ export function copyToClipboard(value, description) {
|
||||
}
|
||||
}
|
||||
|
||||
export function clamp(value, min, max) {
|
||||
export function clamp(value: number, min: number, max: number): number {
|
||||
return value > max ? max : value < min ? min : value
|
||||
}
|
||||
|
||||
export function isMobile() {
|
||||
export function isMobile(): boolean {
|
||||
return navigator.userAgent.includes('Mobile')
|
||||
}
|
||||
|
||||
export function isMac() {
|
||||
export function isMac(): boolean {
|
||||
return navigator.platform.includes('Mac')
|
||||
}
|
||||
|
||||
export function platformCommandControlKey() {
|
||||
export function platformCommandControlKey(): string {
|
||||
return isMac() ? '⌘' : 'Ctrl'
|
||||
}
|
||||
|
||||
export function groupBy<T>(items: T[], groupResolver: (item: T) => string | number): Record<string | number, T[]> {
|
||||
const itemsGrouped: Record<string | number, T[]> = {}
|
||||
for (const item of items) {
|
||||
const group = groupResolver(item)
|
||||
if (!(group in itemsGrouped)) itemsGrouped[group] = [] // Ensure there's an array to push to
|
||||
itemsGrouped[group].push(item)
|
||||
}
|
||||
return itemsGrouped
|
||||
}
|
||||
|
||||
export function uniqueBy<T>(items: T[], uniqueResolver: (item: T) => any): T[] {
|
||||
const uniqueKeysSoFar = new Set<string>()
|
||||
const itemsUnique: T[] = []
|
||||
for (const item of items) {
|
||||
const uniqueKey = uniqueResolver(item)
|
||||
if (!uniqueKeysSoFar.has(uniqueKeysSoFar)) {
|
||||
uniqueKeysSoFar.add(uniqueKey)
|
||||
itemsUnique.push(item)
|
||||
}
|
||||
}
|
||||
return itemsUnique
|
||||
}
|
||||
|
||||
export function sample<T>(items: T[], size: number): T[] {
|
||||
if (size > items.length) throw Error('Sample size cannot exceed items array length!')
|
||||
const results: T[] = []
|
||||
const internalItems = [...items]
|
||||
if (size === items.length) return internalItems
|
||||
for (let i = 0; i < size; i++) {
|
||||
const index = Math.floor(Math.random() * internalItems.length)
|
||||
results.push(internalItems[index])
|
||||
internalItems.splice(index, 1)
|
||||
}
|
||||
return results
|
||||
}
|
@ -6,10 +6,9 @@ import { Button } from 'antd'
|
||||
|
||||
import { useValues, useActions } from 'kea'
|
||||
import { router } from 'kea-router'
|
||||
import _ from 'lodash'
|
||||
|
||||
const isSubmitDisabled = (cohorts) => {
|
||||
if (cohorts && cohorts.groups) return !cohorts.groups.some((group) => !_.isEmpty(group))
|
||||
if (cohorts && cohorts.groups) return !cohorts.groups.some((group) => Object.keys(group).length)
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,6 @@ import { Select } from 'antd'
|
||||
|
||||
import { actionsModel } from '~/models/actionsModel'
|
||||
import { useValues } from 'kea'
|
||||
import _ from 'lodash'
|
||||
|
||||
function DayChoice({ days, name, group, onChange }) {
|
||||
return (
|
||||
@ -74,16 +73,16 @@ export function CohortGroup({ onChange, onRemove, group, index }) {
|
||||
endpoint="person"
|
||||
pageKey={'cohort_' + index}
|
||||
className=" "
|
||||
onChange={(properties) =>
|
||||
onChange={(properties) => {
|
||||
onChange(
|
||||
!_.isEmpty(properties)
|
||||
properties.length
|
||||
? {
|
||||
properties: properties,
|
||||
days: group.days,
|
||||
}
|
||||
: {}
|
||||
)
|
||||
}
|
||||
}}
|
||||
propertyFilters={group.properties || {}}
|
||||
style={{ margin: '1rem 0 0' }}
|
||||
/>
|
||||
|
@ -2,7 +2,7 @@ admin: 0003_logentry_add_action_flag_choices
|
||||
auth: 0011_update_proxy_permissions
|
||||
contenttypes: 0002_remove_content_type_name
|
||||
ee: 0002_hook
|
||||
posthog: 0086_team_session_recording_opt_in
|
||||
posthog: 0087_fix_annotation_created_at
|
||||
rest_hooks: 0002_swappable_hook_model
|
||||
sessions: 0001_initial
|
||||
social_django: 0008_partial_timestamp
|
||||
|
@ -29,9 +29,6 @@
|
||||
"@babel/runtime": "^7.10.4",
|
||||
"@mariusandra/query-selector-shadow-dom": "0.7.2-posthog.2",
|
||||
"@mariusandra/simmerjs": "0.7.1-posthog.1",
|
||||
"@types/lodash": "^4.14.162",
|
||||
"@types/react-syntax-highlighter": "^11.0.4",
|
||||
"@types/zxcvbn": "^4.4.0",
|
||||
"antd": "^4.1.1",
|
||||
"babel-preset-nano-react-app": "^0.1.0",
|
||||
"bootstrap": "^4.4.1",
|
||||
@ -49,7 +46,6 @@
|
||||
"kea-localstorage": "^1.0.2",
|
||||
"kea-router": "^0.4.0",
|
||||
"kea-window-values": "^0.0.1",
|
||||
"lodash": "^4.17.20",
|
||||
"moment": "^2.24.0",
|
||||
"posthog-js": "1.5.0-beta.0",
|
||||
"posthog-js-lite": "^0.0.3",
|
||||
@ -80,6 +76,8 @@
|
||||
"@hot-loader/react-dom": "^16.13.0",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-redux": "^7.1.9",
|
||||
"@types/react-syntax-highlighter": "^11.0.4",
|
||||
"@types/zxcvbn": "^4.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^3.6.0",
|
||||
"@typescript-eslint/parser": "^3.6.0",
|
||||
"autoprefixer": "^9.7.4",
|
||||
|
19
posthog/migrations/0087_fix_annotation_created_at.py
Normal file
19
posthog/migrations/0087_fix_annotation_created_at.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.0.7 on 2020-10-14 07:46
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("posthog", "0086_team_session_recording_opt_in"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="annotation",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, null=True),
|
||||
),
|
||||
]
|
@ -1,4 +1,5 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class Annotation(models.Model):
|
||||
@ -12,7 +13,7 @@ class Annotation(models.Model):
|
||||
GITHUB = "GIT", "GitHub"
|
||||
|
||||
content: models.CharField = models.CharField(max_length=400, null=True, blank=True)
|
||||
created_at: models.DateTimeField = models.DateTimeField(null=True, blank=True)
|
||||
created_at: models.DateTimeField = models.DateTimeField(default=timezone.now, null=True)
|
||||
updated_at: models.DateTimeField = models.DateTimeField(auto_now=True)
|
||||
dashboard_item: models.ForeignKey = models.ForeignKey(
|
||||
"posthog.DashboardItem", on_delete=models.SET_NULL, null=True, blank=True
|
||||
|
@ -1099,11 +1099,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
|
||||
integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
|
||||
|
||||
"@types/lodash@^4.14.162":
|
||||
version "4.14.162"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.162.tgz#65d78c397e0d883f44afbf1f7ba9867022411470"
|
||||
integrity sha512-alvcho1kRUnnD1Gcl4J+hK0eencvzq9rmzvFPRmP5rPHx9VVsJj6bKLTATPVf9ktgv4ujzh7T+XWKp+jhuODig==
|
||||
|
||||
"@types/minimatch@*":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
||||
|
Loading…
Reference in New Issue
Block a user