0
0
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:
Michael Matloka 2020-10-14 10:42:06 +02:00 committed by GitHub
parent ff1fb54eb5
commit c3d3f83c49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 253 additions and 176 deletions

View File

@ -56,7 +56,12 @@ module.exports = {
allowExpressions: true,
},
],
'@typescript-eslint/explicit-module-boundary-types': ['error'],
'@typescript-eslint/explicit-module-boundary-types': [
'error',
{
allowArgumentsExplicitlyTypedAsAny: true,
},
],
},
},
{

View File

@ -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}

View File

@ -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 }) => ({

View File

@ -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) =>

View File

@ -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={() => {

View File

@ -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">&times;</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
}

View File

@ -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
}

View File

@ -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' }}
/>

View File

@ -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

View File

@ -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",

View 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),
),
]

View File

@ -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

View File

@ -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"