mirror of
https://github.com/getredash/redash.git
synced 2025-12-19 17:37:19 -05:00
Compare commits
56 Commits
redis-lock
...
25.10.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0410e2ffe | ||
|
|
7e39b3668d | ||
|
|
92f15a3ccb | ||
|
|
9a1d33381c | ||
|
|
56c06adc24 | ||
|
|
5e8915afe5 | ||
|
|
b8ebf49436 | ||
|
|
59951eda3d | ||
|
|
777153e7a0 | ||
|
|
47b1309f13 | ||
|
|
120250152f | ||
|
|
ac81f0b223 | ||
|
|
7838058953 | ||
|
|
f95156e924 | ||
|
|
74de676bdf | ||
|
|
2762f1fc85 | ||
|
|
438efd0826 | ||
|
|
e586ab708b | ||
|
|
24ca5135aa | ||
|
|
fae354fcce | ||
|
|
4ae372f022 | ||
|
|
0b5907f12b | ||
|
|
00a97d9266 | ||
|
|
35afe880a1 | ||
|
|
a6298f2753 | ||
|
|
e69283f488 | ||
|
|
09ed3c4b81 | ||
|
|
f5e2a4c0fc | ||
|
|
4e200b4a08 | ||
|
|
5ae1f70d9e | ||
|
|
3f781d262b | ||
|
|
a34c1591e3 | ||
|
|
9f76fda18c | ||
|
|
d8ae679937 | ||
|
|
f3b0b60abd | ||
|
|
df8be91a07 | ||
|
|
c9ddd2a7d6 | ||
|
|
6b1e910126 | ||
|
|
14550a9a6c | ||
|
|
b80c5f6a7c | ||
|
|
e46d44f208 | ||
|
|
a1a4bc9d3e | ||
|
|
0900178d24 | ||
|
|
5d31429ca8 | ||
|
|
2f35ceb803 | ||
|
|
8e6c02ecde | ||
|
|
231fd36d46 | ||
|
|
0b6a53a079 | ||
|
|
6167edf97c | ||
|
|
4ed0ad3c9c | ||
|
|
2375f0b05f | ||
|
|
eced377ae4 | ||
|
|
84262fe143 | ||
|
|
612eb8c630 | ||
|
|
866fb48afb | ||
|
|
353776e8e1 |
3
.github/workflows/periodic-snapshot.yml
vendored
3
.github/workflows/periodic-snapshot.yml
vendored
@@ -2,7 +2,7 @@ name: Periodic Snapshot
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '10 0 1 * *' # 10 minutes after midnight on the first of every month
|
- cron: '10 0 1 * *' # 10 minutes after midnight on the first day of every month
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
bump:
|
bump:
|
||||||
@@ -24,6 +24,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
bump-version-and-tag:
|
bump-version-and-tag:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.ref_name == github.event.repository.default_branch
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
23
.github/workflows/preview-image.yml
vendored
23
.github/workflows/preview-image.yml
vendored
@@ -32,6 +32,9 @@ jobs:
|
|||||||
elif [[ "${{ secrets.DOCKER_PASS }}" == '' ]]; then
|
elif [[ "${{ secrets.DOCKER_PASS }}" == '' ]]; then
|
||||||
echo 'Docker password is empty. Skipping build+push'
|
echo 'Docker password is empty. Skipping build+push'
|
||||||
echo skip=true >> "$GITHUB_OUTPUT"
|
echo skip=true >> "$GITHUB_OUTPUT"
|
||||||
|
elif [[ "${{ vars.DOCKER_REPOSITORY }}" == '' ]]; then
|
||||||
|
echo 'Docker repository is empty. Skipping build+push'
|
||||||
|
echo skip=true >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo 'Docker user and password are set and branch is `master`.'
|
echo 'Docker user and password are set and branch is `master`.'
|
||||||
echo 'Building + pushing `preview` image.'
|
echo 'Building + pushing `preview` image.'
|
||||||
@@ -97,8 +100,8 @@ jobs:
|
|||||||
if: ${{ github.event.inputs.dockerRepository == 'preview' || !github.event.workflow_run }}
|
if: ${{ github.event.inputs.dockerRepository == 'preview' || !github.event.workflow_run }}
|
||||||
with:
|
with:
|
||||||
tags: |
|
tags: |
|
||||||
${{ vars.DOCKER_USER }}/redash
|
${{ vars.DOCKER_REPOSITORY }}/redash
|
||||||
${{ vars.DOCKER_USER }}/preview
|
${{ vars.DOCKER_REPOSITORY }}/preview
|
||||||
context: .
|
context: .
|
||||||
build-args: |
|
build-args: |
|
||||||
test_all_deps=true
|
test_all_deps=true
|
||||||
@@ -114,11 +117,11 @@ jobs:
|
|||||||
if: ${{ github.event.inputs.dockerRepository == 'redash' }}
|
if: ${{ github.event.inputs.dockerRepository == 'redash' }}
|
||||||
with:
|
with:
|
||||||
tags: |
|
tags: |
|
||||||
${{ vars.DOCKER_USER }}/redash:${{ steps.version.outputs.VERSION_TAG }}
|
${{ vars.DOCKER_REPOSITORY }}/redash:${{ steps.version.outputs.VERSION_TAG }}
|
||||||
context: .
|
context: .
|
||||||
build-args: |
|
build-args: |
|
||||||
test_all_deps=true
|
test_all_deps=true
|
||||||
outputs: type=image,push-by-digest=true,push=true
|
outputs: type=image,push-by-digest=false,push=true
|
||||||
cache-from: type=gha,scope=${{ matrix.arch }}
|
cache-from: type=gha,scope=${{ matrix.arch }}
|
||||||
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||||
env:
|
env:
|
||||||
@@ -169,14 +172,14 @@ jobs:
|
|||||||
if: ${{ github.event.inputs.dockerRepository == 'preview' || !github.event.workflow_run }}
|
if: ${{ github.event.inputs.dockerRepository == 'preview' || !github.event.workflow_run }}
|
||||||
working-directory: ${{ runner.temp }}/digests
|
working-directory: ${{ runner.temp }}/digests
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools create -t ${{ vars.DOCKER_USER }}/redash:preview \
|
docker buildx imagetools create -t ${{ vars.DOCKER_REPOSITORY }}/redash:preview \
|
||||||
$(printf '${{ vars.DOCKER_USER }}/redash:preview@sha256:%s ' *)
|
$(printf '${{ vars.DOCKER_REPOSITORY }}/redash:preview@sha256:%s ' *)
|
||||||
docker buildx imagetools create -t ${{ vars.DOCKER_USER }}/preview:${{ needs.build-docker-image.outputs.VERSION_TAG }} \
|
docker buildx imagetools create -t ${{ vars.DOCKER_REPOSITORY }}/preview:${{ needs.build-docker-image.outputs.VERSION_TAG }} \
|
||||||
$(printf '${{ vars.DOCKER_USER }}/preview:${{ needs.build-docker-image.outputs.VERSION_TAG }}@sha256:%s ' *)
|
$(printf '${{ vars.DOCKER_REPOSITORY }}/preview:${{ needs.build-docker-image.outputs.VERSION_TAG }}@sha256:%s ' *)
|
||||||
|
|
||||||
- name: Create and push manifest for the release image
|
- name: Create and push manifest for the release image
|
||||||
if: ${{ github.event.inputs.dockerRepository == 'redash' }}
|
if: ${{ github.event.inputs.dockerRepository == 'redash' }}
|
||||||
working-directory: ${{ runner.temp }}/digests
|
working-directory: ${{ runner.temp }}/digests
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools create -t ${{ vars.DOCKER_USER }}/redash:${{ needs.build-docker-image.outputs.VERSION_TAG }} \
|
docker buildx imagetools create -t ${{ vars.DOCKER_REPOSITORY }}/redash:${{ needs.build-docker-image.outputs.VERSION_TAG }} \
|
||||||
$(printf '${{ vars.DOCKER_USER }}/redash:${{ needs.build-docker-image.outputs.VERSION_TAG }}@sha256:%s ' *)
|
$(printf '${{ vars.DOCKER_REPOSITORY }}/redash:${{ needs.build-docker-image.outputs.VERSION_TAG }}@sha256:%s ' *)
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ EOF
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV POETRY_VERSION=1.8.3
|
ENV POETRY_VERSION=2.1.4
|
||||||
ENV POETRY_HOME=/etc/poetry
|
ENV POETRY_HOME=/etc/poetry
|
||||||
ENV POETRY_VIRTUALENVS_CREATE=false
|
ENV POETRY_VIRTUALENVS_CREATE=false
|
||||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ server() {
|
|||||||
MAX_REQUESTS=${MAX_REQUESTS:-1000}
|
MAX_REQUESTS=${MAX_REQUESTS:-1000}
|
||||||
MAX_REQUESTS_JITTER=${MAX_REQUESTS_JITTER:-100}
|
MAX_REQUESTS_JITTER=${MAX_REQUESTS_JITTER:-100}
|
||||||
TIMEOUT=${REDASH_GUNICORN_TIMEOUT:-60}
|
TIMEOUT=${REDASH_GUNICORN_TIMEOUT:-60}
|
||||||
exec /usr/local/bin/gunicorn -b 0.0.0.0:5000 --name redash -w${REDASH_WEB_WORKERS:-4} redash.wsgi:app --max-requests $MAX_REQUESTS --max-requests-jitter $MAX_REQUESTS_JITTER --timeout $TIMEOUT
|
exec /usr/local/bin/gunicorn -b 0.0.0.0:5000 --name redash -w${REDASH_WEB_WORKERS:-4} redash.wsgi:app --max-requests $MAX_REQUESTS --max-requests-jitter $MAX_REQUESTS_JITTER --timeout $TIMEOUT --limit-request-line ${REDASH_GUNICORN_LIMIT_REQUEST_LINE:-0}
|
||||||
}
|
}
|
||||||
|
|
||||||
create_db() {
|
create_db() {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ body {
|
|||||||
display: table;
|
display: table;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
height: calc(100vh - 116px);
|
height: calc(100% - 116px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ html {
|
|||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -35,7 +35,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#application-root {
|
#application-root {
|
||||||
min-height: 100vh;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#application-root,
|
#application-root,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,4 +135,4 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ body.fixed-layout {
|
|||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
|
|
||||||
.application-layout-content > div {
|
.application-layout-content > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -90,7 +90,7 @@ body.fixed-layout {
|
|||||||
.embed__vis {
|
.embed__vis {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
height: calc(~'100vh - 25px');
|
height: calc(~'100% - 25px');
|
||||||
|
|
||||||
> .embed-heading {
|
> .embed-heading {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ body #application-root {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
|
|
||||||
.application-layout-side-menu {
|
.application-layout-side-menu {
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@media @mobileBreakpoint {
|
@media @mobileBreakpoint {
|
||||||
@@ -47,6 +47,10 @@ body #application-root {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body > section {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
body.fixed-layout #application-root {
|
body.fixed-layout #application-root {
|
||||||
.application-layout-content {
|
.application-layout-content {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
background: linear-gradient(to bottom, transparent, transparent 2px, #f6f8f9 2px, #f6f8f9 5px),
|
background: linear-gradient(to bottom, transparent, transparent 2px, #f6f8f9 2px, #f6f8f9 5px),
|
||||||
linear-gradient(to left, #b3babf, #b3babf 1px, transparent 1px, transparent);
|
linear-gradient(to left, #b3babf, #b3babf 1px, transparent 1px, transparent);
|
||||||
background-size: calc((100% + 15px) / 6) 5px;
|
background-size: calc((100% + 15px) / 12) 5px;
|
||||||
background-position: -7px 1px;
|
background-position: -7px 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ export interface PaginationOptions {
|
|||||||
itemsPerPage?: number;
|
itemsPerPage?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SearchOptions {
|
||||||
|
isServerSideFTS?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Controller<I, P = any> {
|
export interface Controller<I, P = any> {
|
||||||
params: P; // TODO: Find out what params is (except merging with props)
|
params: P; // TODO: Find out what params is (except merging with props)
|
||||||
|
|
||||||
@@ -18,7 +22,7 @@ export interface Controller<I, P = any> {
|
|||||||
|
|
||||||
// search
|
// search
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
updateSearch: (searchTerm: string) => void;
|
updateSearch: (searchTerm: string, searchOptions?: SearchOptions) => void;
|
||||||
|
|
||||||
// tags
|
// tags
|
||||||
selectedTags: string[];
|
selectedTags: string[];
|
||||||
@@ -28,6 +32,7 @@ export interface Controller<I, P = any> {
|
|||||||
orderByField?: string;
|
orderByField?: string;
|
||||||
orderByReverse: boolean;
|
orderByReverse: boolean;
|
||||||
toggleSorting: (orderByField: string) => void;
|
toggleSorting: (orderByField: string) => void;
|
||||||
|
setSorting: (orderByField: string, orderByReverse: boolean) => void;
|
||||||
|
|
||||||
// pagination
|
// pagination
|
||||||
page: number;
|
page: number;
|
||||||
@@ -93,7 +98,7 @@ export interface ItemsListWrappedComponentProps<I, P = any> {
|
|||||||
export function wrap<I, P = any>(
|
export function wrap<I, P = any>(
|
||||||
WrappedComponent: React.ComponentType<ItemsListWrappedComponentProps<I>>,
|
WrappedComponent: React.ComponentType<ItemsListWrappedComponentProps<I>>,
|
||||||
createItemsSource: () => ItemsSource,
|
createItemsSource: () => ItemsSource,
|
||||||
createStateStorage: () => StateStorage
|
createStateStorage: ( { ...props }) => StateStorage
|
||||||
) {
|
) {
|
||||||
class ItemsListWrapper extends React.Component<ItemsListWrapperProps, ItemsListWrapperState<I, P>> {
|
class ItemsListWrapper extends React.Component<ItemsListWrapperProps, ItemsListWrapperState<I, P>> {
|
||||||
private _itemsSource: ItemsSource;
|
private _itemsSource: ItemsSource;
|
||||||
@@ -116,7 +121,7 @@ export function wrap<I, P = any>(
|
|||||||
constructor(props: ItemsListWrapperProps) {
|
constructor(props: ItemsListWrapperProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const stateStorage = createStateStorage();
|
const stateStorage = createStateStorage({ ...props });
|
||||||
const itemsSource = createItemsSource();
|
const itemsSource = createItemsSource();
|
||||||
this._itemsSource = itemsSource;
|
this._itemsSource = itemsSource;
|
||||||
|
|
||||||
@@ -139,10 +144,11 @@ export function wrap<I, P = any>(
|
|||||||
this.props.onError!(error);
|
this.props.onError!(error);
|
||||||
|
|
||||||
const initialState = this.getState({ ...itemsSource.getState(), isLoaded: false });
|
const initialState = this.getState({ ...itemsSource.getState(), isLoaded: false });
|
||||||
const { updatePagination, toggleSorting, updateSearch, updateSelectedTags, update, handleError } = itemsSource;
|
const { updatePagination, toggleSorting, setSorting, updateSearch, updateSelectedTags, update, handleError } = itemsSource;
|
||||||
this.state = {
|
this.state = {
|
||||||
...initialState,
|
...initialState,
|
||||||
toggleSorting, // eslint-disable-line react/no-unused-state
|
toggleSorting, // eslint-disable-line react/no-unused-state
|
||||||
|
setSorting, // eslint-disable-line react/no-unused-state
|
||||||
updateSearch: debounce(updateSearch, 200), // eslint-disable-line react/no-unused-state
|
updateSearch: debounce(updateSearch, 200), // eslint-disable-line react/no-unused-state
|
||||||
updateSelectedTags, // eslint-disable-line react/no-unused-state
|
updateSelectedTags, // eslint-disable-line react/no-unused-state
|
||||||
updatePagination, // eslint-disable-line react/no-unused-state
|
updatePagination, // eslint-disable-line react/no-unused-state
|
||||||
|
|||||||
@@ -39,14 +39,12 @@ export class ItemsSource {
|
|||||||
const customParams = {};
|
const customParams = {};
|
||||||
const context = {
|
const context = {
|
||||||
...this.getCallbackContext(),
|
...this.getCallbackContext(),
|
||||||
setCustomParams: params => {
|
setCustomParams: (params) => {
|
||||||
extend(customParams, params);
|
extend(customParams, params);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return this._beforeUpdate().then(() => {
|
return this._beforeUpdate().then(() => {
|
||||||
const fetchToken = Math.random()
|
const fetchToken = Math.random().toString(36).substr(2);
|
||||||
.toString(36)
|
|
||||||
.substr(2);
|
|
||||||
this._currentFetchToken = fetchToken;
|
this._currentFetchToken = fetchToken;
|
||||||
return this._fetcher
|
return this._fetcher
|
||||||
.fetch(changes, state, context)
|
.fetch(changes, state, context)
|
||||||
@@ -59,7 +57,7 @@ export class ItemsSource {
|
|||||||
return this._afterUpdate();
|
return this._afterUpdate();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => this.handleError(error));
|
.catch((error) => this.handleError(error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,19 +122,26 @@ export class ItemsSource {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleSorting = orderByField => {
|
toggleSorting = (orderByField) => {
|
||||||
this._sorter.toggleField(orderByField);
|
this._sorter.toggleField(orderByField);
|
||||||
this._savedOrderByField = this._sorter.field;
|
this._savedOrderByField = this._sorter.field;
|
||||||
this._changed({ sorting: true });
|
this._changed({ sorting: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
updateSearch = searchTerm => {
|
setSorting = (orderByField, orderByReverse) => {
|
||||||
|
this._sorter.setField(orderByField);
|
||||||
|
this._sorter.setReverse(orderByReverse);
|
||||||
|
this._savedOrderByField = this._sorter.field;
|
||||||
|
this._changed({ sorting: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
updateSearch = (searchTerm, options) => {
|
||||||
// here we update state directly, but later `fetchData` will update it properly
|
// here we update state directly, but later `fetchData` will update it properly
|
||||||
this._searchTerm = searchTerm;
|
this._searchTerm = searchTerm;
|
||||||
// in search mode ignore the ordering and use the ranking order
|
// in search mode ignore the ordering and use the ranking order
|
||||||
// provided by the server-side FTS backend instead, unless it was
|
// provided by the server-side FTS backend instead, unless it was
|
||||||
// requested by the user by actively ordering in search mode
|
// requested by the user by actively ordering in search mode
|
||||||
if (searchTerm === "") {
|
if (searchTerm === "" || !options?.isServerSideFTS) {
|
||||||
this._sorter.setField(this._savedOrderByField); // restore ordering
|
this._sorter.setField(this._savedOrderByField); // restore ordering
|
||||||
} else {
|
} else {
|
||||||
this._sorter.setField(null);
|
this._sorter.setField(null);
|
||||||
@@ -145,7 +150,7 @@ export class ItemsSource {
|
|||||||
this._changed({ search: true, pagination: { page: true } });
|
this._changed({ search: true, pagination: { page: true } });
|
||||||
};
|
};
|
||||||
|
|
||||||
updateSelectedTags = selectedTags => {
|
updateSelectedTags = (selectedTags) => {
|
||||||
this._selectedTags = selectedTags;
|
this._selectedTags = selectedTags;
|
||||||
this._paginator.setPage(1);
|
this._paginator.setPage(1);
|
||||||
this._changed({ tags: true, pagination: { page: true } });
|
this._changed({ tags: true, pagination: { page: true } });
|
||||||
@@ -153,7 +158,7 @@ export class ItemsSource {
|
|||||||
|
|
||||||
update = () => this._changed();
|
update = () => this._changed();
|
||||||
|
|
||||||
handleError = error => {
|
handleError = (error) => {
|
||||||
if (isFunction(this.onError)) {
|
if (isFunction(this.onError)) {
|
||||||
this.onError(error);
|
this.onError(error);
|
||||||
}
|
}
|
||||||
@@ -172,7 +177,7 @@ export class ResourceItemsSource extends ItemsSource {
|
|||||||
processResults: (results, context) => {
|
processResults: (results, context) => {
|
||||||
let processItem = getItemProcessor(context);
|
let processItem = getItemProcessor(context);
|
||||||
processItem = isFunction(processItem) ? processItem : identity;
|
processItem = isFunction(processItem) ? processItem : identity;
|
||||||
return map(results, item => processItem(item, context));
|
return map(results, (item) => processItem(item, context));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const Columns = {
|
|||||||
date(overrides) {
|
date(overrides) {
|
||||||
return extend(
|
return extend(
|
||||||
{
|
{
|
||||||
render: text => formatDate(text),
|
render: (text) => formatDate(text),
|
||||||
},
|
},
|
||||||
overrides
|
overrides
|
||||||
);
|
);
|
||||||
@@ -52,7 +52,7 @@ export const Columns = {
|
|||||||
dateTime(overrides) {
|
dateTime(overrides) {
|
||||||
return extend(
|
return extend(
|
||||||
{
|
{
|
||||||
render: text => formatDateTime(text),
|
render: (text) => formatDateTime(text),
|
||||||
},
|
},
|
||||||
overrides
|
overrides
|
||||||
);
|
);
|
||||||
@@ -62,7 +62,7 @@ export const Columns = {
|
|||||||
{
|
{
|
||||||
width: "1%",
|
width: "1%",
|
||||||
className: "text-nowrap",
|
className: "text-nowrap",
|
||||||
render: text => durationHumanize(text),
|
render: (text) => durationHumanize(text),
|
||||||
},
|
},
|
||||||
overrides
|
overrides
|
||||||
);
|
);
|
||||||
@@ -70,7 +70,7 @@ export const Columns = {
|
|||||||
timeAgo(overrides, timeAgoCustomProps = undefined) {
|
timeAgo(overrides, timeAgoCustomProps = undefined) {
|
||||||
return extend(
|
return extend(
|
||||||
{
|
{
|
||||||
render: value => <TimeAgo date={value} {...timeAgoCustomProps} />,
|
render: (value) => <TimeAgo date={value} {...timeAgoCustomProps} />,
|
||||||
},
|
},
|
||||||
overrides
|
overrides
|
||||||
);
|
);
|
||||||
@@ -110,6 +110,7 @@ export default class ItemsTable extends React.Component {
|
|||||||
orderByField: PropTypes.string,
|
orderByField: PropTypes.string,
|
||||||
orderByReverse: PropTypes.bool,
|
orderByReverse: PropTypes.bool,
|
||||||
toggleSorting: PropTypes.func,
|
toggleSorting: PropTypes.func,
|
||||||
|
setSorting: PropTypes.func,
|
||||||
"data-test": PropTypes.string,
|
"data-test": PropTypes.string,
|
||||||
rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||||
};
|
};
|
||||||
@@ -127,18 +128,15 @@ export default class ItemsTable extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
prepareColumns() {
|
prepareColumns() {
|
||||||
const { orderByField, orderByReverse, toggleSorting } = this.props;
|
const { orderByField, orderByReverse } = this.props;
|
||||||
const orderByDirection = orderByReverse ? "descend" : "ascend";
|
const orderByDirection = orderByReverse ? "descend" : "ascend";
|
||||||
|
|
||||||
return map(
|
return map(
|
||||||
map(
|
map(
|
||||||
filter(this.props.columns, column => (isFunction(column.isAvailable) ? column.isAvailable() : true)),
|
filter(this.props.columns, (column) => (isFunction(column.isAvailable) ? column.isAvailable() : true)),
|
||||||
column => extend(column, { orderByField: column.orderByField || column.field })
|
(column) => extend(column, { orderByField: column.orderByField || column.field })
|
||||||
),
|
),
|
||||||
(column, index) => {
|
(column, index) => {
|
||||||
// Bind click events only to sortable columns
|
|
||||||
const onHeaderCell = column.sorter ? () => ({ onClick: () => toggleSorting(column.orderByField) }) : null;
|
|
||||||
|
|
||||||
// Wrap render function to pass correct arguments
|
// Wrap render function to pass correct arguments
|
||||||
const render = isFunction(column.render) ? (text, row) => column.render(text, row.item) : identity;
|
const render = isFunction(column.render) ? (text, row) => column.render(text, row.item) : identity;
|
||||||
|
|
||||||
@@ -146,14 +144,13 @@ export default class ItemsTable extends React.Component {
|
|||||||
key: "column" + index,
|
key: "column" + index,
|
||||||
dataIndex: ["item", column.field],
|
dataIndex: ["item", column.field],
|
||||||
defaultSortOrder: column.orderByField === orderByField ? orderByDirection : null,
|
defaultSortOrder: column.orderByField === orderByField ? orderByDirection : null,
|
||||||
onHeaderCell,
|
|
||||||
render,
|
render,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRowKey = record => {
|
getRowKey = (record) => {
|
||||||
const { rowKey } = this.props;
|
const { rowKey } = this.props;
|
||||||
if (rowKey) {
|
if (rowKey) {
|
||||||
if (isFunction(rowKey)) {
|
if (isFunction(rowKey)) {
|
||||||
@@ -172,22 +169,43 @@ export default class ItemsTable extends React.Component {
|
|||||||
|
|
||||||
// Bind events only if `onRowClick` specified
|
// Bind events only if `onRowClick` specified
|
||||||
const onTableRow = isFunction(this.props.onRowClick)
|
const onTableRow = isFunction(this.props.onRowClick)
|
||||||
? row => ({
|
? (row) => ({
|
||||||
onClick: event => {
|
onClick: (event) => {
|
||||||
this.props.onRowClick(event, row.item);
|
this.props.onRowClick(event, row.item);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const onChange = (pagination, filters, sorter, extra) => {
|
||||||
|
const action = extra?.action;
|
||||||
|
if (action === "sort") {
|
||||||
|
const propsColumn = this.props.columns.find((column) => column.field === sorter.field[1]);
|
||||||
|
if (!propsColumn.sorter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let orderByField = propsColumn.orderByField;
|
||||||
|
const orderByReverse = sorter.order === "descend";
|
||||||
|
|
||||||
|
if (orderByReverse === undefined) {
|
||||||
|
orderByField = null;
|
||||||
|
}
|
||||||
|
if (this.props.setSorting) {
|
||||||
|
this.props.setSorting(orderByField, orderByReverse);
|
||||||
|
} else {
|
||||||
|
this.props.toggleSorting(orderByField);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const { showHeader } = this.props;
|
const { showHeader } = this.props;
|
||||||
if (this.props.loading) {
|
if (this.props.loading) {
|
||||||
if (isEmpty(tableDataProps.dataSource)) {
|
if (isEmpty(tableDataProps.dataSource)) {
|
||||||
tableDataProps.columns = tableDataProps.columns.map(column => ({
|
tableDataProps.columns = tableDataProps.columns.map((column) => ({
|
||||||
...column,
|
...column,
|
||||||
sorter: false,
|
sorter: false,
|
||||||
render: () => <Skeleton active paragraph={false} />,
|
render: () => <Skeleton active paragraph={false} />,
|
||||||
}));
|
}));
|
||||||
tableDataProps.dataSource = range(10).map(key => ({ key: `${key}` }));
|
tableDataProps.dataSource = range(10).map((key) => ({ key: `${key}` }));
|
||||||
} else {
|
} else {
|
||||||
tableDataProps.loading = { indicator: null };
|
tableDataProps.loading = { indicator: null };
|
||||||
}
|
}
|
||||||
@@ -200,6 +218,7 @@ export default class ItemsTable extends React.Component {
|
|||||||
rowKey={this.getRowKey}
|
rowKey={this.getRowKey}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
onRow={onTableRow}
|
onRow={onTableRow}
|
||||||
|
onChange={onChange}
|
||||||
data-test={this.props["data-test"]}
|
data-test={this.props["data-test"]}
|
||||||
{...tableDataProps}
|
{...tableDataProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -47,20 +47,30 @@ function SchemaItem({ item, expanded, onToggle, onSelect, ...props }) {
|
|||||||
return (
|
return (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
<div className="schema-list-item">
|
<div className="schema-list-item">
|
||||||
<PlainButton className="table-name" onClick={onToggle}>
|
<Tooltip
|
||||||
<i className="fa fa-table m-r-5" aria-hidden="true" />
|
title={item.description}
|
||||||
<strong>
|
mouseEnterDelay={0}
|
||||||
<span title={item.name}>{tableDisplayName}</span>
|
mouseLeaveDelay={0}
|
||||||
{!isNil(item.size) && <span> ({item.size})</span>}
|
placement="rightTop"
|
||||||
</strong>
|
trigger={item.description ? "hover" : ""}
|
||||||
</PlainButton>
|
overlayStyle={{ whiteSpace: "pre-line" }}
|
||||||
|
>
|
||||||
|
<PlainButton className="table-name" onClick={onToggle}>
|
||||||
|
<i className="fa fa-table m-r-5" aria-hidden="true" />
|
||||||
|
<strong>
|
||||||
|
<span title={item.name}>{tableDisplayName}</span>
|
||||||
|
{!isNil(item.size) && <span> ({item.size})</span>}
|
||||||
|
</strong>
|
||||||
|
</PlainButton>
|
||||||
|
</Tooltip>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title="Insert table name into query text"
|
title="Insert table name into query text"
|
||||||
mouseEnterDelay={0}
|
mouseEnterDelay={0}
|
||||||
mouseLeaveDelay={0}
|
mouseLeaveDelay={0}
|
||||||
placement="topRight"
|
placement="topRight"
|
||||||
arrowPointAtCenter>
|
arrowPointAtCenter
|
||||||
<PlainButton className="copy-to-editor" onClick={e => handleSelect(e, item.name)}>
|
>
|
||||||
|
<PlainButton className="copy-to-editor" onClick={(e) => handleSelect(e, item.name)}>
|
||||||
<i className="fa fa-angle-double-right" aria-hidden="true" />
|
<i className="fa fa-angle-double-right" aria-hidden="true" />
|
||||||
</PlainButton>
|
</PlainButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -70,16 +80,22 @@ function SchemaItem({ item, expanded, onToggle, onSelect, ...props }) {
|
|||||||
{item.loading ? (
|
{item.loading ? (
|
||||||
<div className="table-open">Loading...</div>
|
<div className="table-open">Loading...</div>
|
||||||
) : (
|
) : (
|
||||||
map(item.columns, column => {
|
map(item.columns, (column) => {
|
||||||
const columnName = get(column, "name");
|
const columnName = get(column, "name");
|
||||||
const columnType = get(column, "type");
|
const columnType = get(column, "type");
|
||||||
|
const columnDescription = get(column, "description");
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title="Insert column name into query text"
|
title={"Insert column name into query text" + (columnDescription ? "\n" + columnDescription : "")}
|
||||||
mouseEnterDelay={0}
|
mouseEnterDelay={0}
|
||||||
mouseLeaveDelay={0}
|
mouseLeaveDelay={0}
|
||||||
placement="rightTop">
|
placement="rightTop"
|
||||||
<PlainButton key={columnName} className="table-open-item" onClick={e => handleSelect(e, columnName)}>
|
>
|
||||||
|
<PlainButton
|
||||||
|
key={columnName}
|
||||||
|
className="table-open-item"
|
||||||
|
onClick={(e) => handleSelect(e, columnName)}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
{columnName} {columnType && <span className="column-type">{columnType}</span>}
|
{columnName} {columnType && <span className="column-type">{columnType}</span>}
|
||||||
</div>
|
</div>
|
||||||
@@ -168,7 +184,7 @@ export function SchemaList({ loading, schema, expandedFlags, onTableExpand, onIt
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function applyFilterOnSchema(schema, filterString) {
|
export function applyFilterOnSchema(schema, filterString) {
|
||||||
const filters = filter(filterString.toLowerCase().split(/\s+/), s => s.length > 0);
|
const filters = filter(filterString.toLowerCase().split(/\s+/), (s) => s.length > 0);
|
||||||
|
|
||||||
// Empty string: return original schema
|
// Empty string: return original schema
|
||||||
if (filters.length === 0) {
|
if (filters.length === 0) {
|
||||||
@@ -181,9 +197,9 @@ export function applyFilterOnSchema(schema, filterString) {
|
|||||||
const columnFilter = filters[0];
|
const columnFilter = filters[0];
|
||||||
return filter(
|
return filter(
|
||||||
schema,
|
schema,
|
||||||
item =>
|
(item) =>
|
||||||
includes(item.name.toLowerCase(), nameFilter) ||
|
includes(item.name.toLowerCase(), nameFilter) ||
|
||||||
some(item.columns, column => includes(get(column, "name").toLowerCase(), columnFilter))
|
some(item.columns, (column) => includes(get(column, "name").toLowerCase(), columnFilter))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,11 +207,11 @@ export function applyFilterOnSchema(schema, filterString) {
|
|||||||
const nameFilter = filters[0];
|
const nameFilter = filters[0];
|
||||||
const columnFilter = filters[1];
|
const columnFilter = filters[1];
|
||||||
return filter(
|
return filter(
|
||||||
map(schema, item => {
|
map(schema, (item) => {
|
||||||
if (includes(item.name.toLowerCase(), nameFilter)) {
|
if (includes(item.name.toLowerCase(), nameFilter)) {
|
||||||
item = {
|
item = {
|
||||||
...item,
|
...item,
|
||||||
columns: filter(item.columns, column => includes(get(column, "name").toLowerCase(), columnFilter)),
|
columns: filter(item.columns, (column) => includes(get(column, "name").toLowerCase(), columnFilter)),
|
||||||
};
|
};
|
||||||
return item.columns.length > 0 ? item : null;
|
return item.columns.length > 0 ? item : null;
|
||||||
}
|
}
|
||||||
@@ -243,7 +259,7 @@ export default function SchemaBrowser({
|
|||||||
placeholder="Search schema..."
|
placeholder="Search schema..."
|
||||||
aria-label="Search schema"
|
aria-label="Search schema"
|
||||||
disabled={schema.length === 0}
|
disabled={schema.length === 0}
|
||||||
onChange={event => handleFilterChange(event.target.value)}
|
onChange={(event) => handleFilterChange(event.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tooltip title="Refresh Schema">
|
<Tooltip title="Refresh Schema">
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ function wrapComponentWithSettings(WrappedComponent) {
|
|||||||
"dateTimeFormat",
|
"dateTimeFormat",
|
||||||
"integerFormat",
|
"integerFormat",
|
||||||
"floatFormat",
|
"floatFormat",
|
||||||
|
"nullValue",
|
||||||
"booleanValues",
|
"booleanValues",
|
||||||
"tableCellMaxJSONSize",
|
"tableCellMaxJSONSize",
|
||||||
"allowCustomJSVisualizations",
|
"allowCustomJSVisualizations",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
export default {
|
export default {
|
||||||
columns: 6, // grid columns count
|
columns: 12, // grid columns count
|
||||||
rowHeight: 50, // grid row height (incl. bottom padding)
|
rowHeight: 50, // grid row height (incl. bottom padding)
|
||||||
margins: 15, // widget margins
|
margins: 15, // widget margins
|
||||||
mobileBreakPoint: 800,
|
mobileBreakPoint: 800,
|
||||||
// defaults for widgets
|
// defaults for widgets
|
||||||
defaultSizeX: 3,
|
defaultSizeX: 6,
|
||||||
defaultSizeY: 3,
|
defaultSizeY: 3,
|
||||||
minSizeX: 1,
|
minSizeX: 2,
|
||||||
maxSizeX: 6,
|
maxSizeX: 12,
|
||||||
minSizeY: 1,
|
minSizeY: 2,
|
||||||
maxSizeY: 1000,
|
maxSizeY: 1000,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" translate="no">
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
|||||||
@@ -81,12 +81,19 @@ function DashboardListExtraActions(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DashboardList({ controller }) {
|
function DashboardList({ controller }) {
|
||||||
|
let usedListColumns = listColumns;
|
||||||
|
if (controller.params.currentPage === "favorites") {
|
||||||
|
usedListColumns = [
|
||||||
|
...usedListColumns,
|
||||||
|
Columns.dateTime.sortable({ title: "Starred At", field: "starred_at", width: "1%" }),
|
||||||
|
];
|
||||||
|
}
|
||||||
const {
|
const {
|
||||||
areExtraActionsAvailable,
|
areExtraActionsAvailable,
|
||||||
listColumns: tableColumns,
|
listColumns: tableColumns,
|
||||||
Component: ExtraActionsComponent,
|
Component: ExtraActionsComponent,
|
||||||
selectedItems,
|
selectedItems,
|
||||||
} = useItemsListExtraActions(controller, listColumns, DashboardListExtraActions);
|
} = useItemsListExtraActions(controller, usedListColumns, DashboardListExtraActions);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-dashboard-list">
|
<div className="page-dashboard-list">
|
||||||
@@ -139,9 +146,9 @@ function DashboardList({ controller }) {
|
|||||||
showPageSizeSelect
|
showPageSizeSelect
|
||||||
totalCount={controller.totalItemsCount}
|
totalCount={controller.totalItemsCount}
|
||||||
pageSize={controller.itemsPerPage}
|
pageSize={controller.itemsPerPage}
|
||||||
onPageSizeChange={itemsPerPage => controller.updatePagination({ itemsPerPage })}
|
onPageSizeChange={(itemsPerPage) => controller.updatePagination({ itemsPerPage })}
|
||||||
page={controller.page}
|
page={controller.page}
|
||||||
onChange={page => controller.updatePagination({ page })}
|
onChange={(page) => controller.updatePagination({ page })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
@@ -170,10 +177,10 @@ const DashboardListPage = itemsList(
|
|||||||
}[currentPage];
|
}[currentPage];
|
||||||
},
|
},
|
||||||
getItemProcessor() {
|
getItemProcessor() {
|
||||||
return item => new Dashboard(item);
|
return (item) => new Dashboard(item);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
() => new UrlStateStorage({ orderByField: "created_at", orderByReverse: true })
|
({ ...props }) => new UrlStateStorage({ orderByField: props.orderByField ?? "created_at", orderByReverse: true })
|
||||||
);
|
);
|
||||||
|
|
||||||
routes.register(
|
routes.register(
|
||||||
@@ -181,7 +188,7 @@ routes.register(
|
|||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/dashboards",
|
path: "/dashboards",
|
||||||
title: "Dashboards",
|
title: "Dashboards",
|
||||||
render: pageProps => <DashboardListPage {...pageProps} currentPage="all" />,
|
render: (pageProps) => <DashboardListPage {...pageProps} currentPage="all" />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
routes.register(
|
routes.register(
|
||||||
@@ -189,7 +196,7 @@ routes.register(
|
|||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/dashboards/favorites",
|
path: "/dashboards/favorites",
|
||||||
title: "Favorite Dashboards",
|
title: "Favorite Dashboards",
|
||||||
render: pageProps => <DashboardListPage {...pageProps} currentPage="favorites" />,
|
render: (pageProps) => <DashboardListPage {...pageProps} currentPage="favorites" orderByField="starred_at" />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
routes.register(
|
routes.register(
|
||||||
@@ -197,6 +204,6 @@ routes.register(
|
|||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/dashboards/my",
|
path: "/dashboards/my",
|
||||||
title: "My Dashboards",
|
title: "My Dashboards",
|
||||||
render: pageProps => <DashboardListPage {...pageProps} currentPage="my" />,
|
render: (pageProps) => <DashboardListPage {...pageProps} currentPage="my" />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .container {
|
> .container {
|
||||||
min-height: calc(100vh - 95px);
|
min-height: calc(100% - 95px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-message {
|
.loading-message {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function FavoriteList({ title, resource, itemUrl, emptyState }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
resource
|
resource
|
||||||
.favorites()
|
.favorites({ order: "-starred_at" })
|
||||||
.then(({ results }) => setItems(results))
|
.then(({ results }) => setItems(results))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [resource]);
|
}, [resource]);
|
||||||
@@ -28,7 +28,7 @@ export function FavoriteList({ title, resource, itemUrl, emptyState }) {
|
|||||||
</div>
|
</div>
|
||||||
{!isEmpty(items) && (
|
{!isEmpty(items) && (
|
||||||
<div role="list" className="list-group">
|
<div role="list" className="list-group">
|
||||||
{items.map(item => (
|
{items.map((item) => (
|
||||||
<Link key={itemUrl(item)} role="listitem" className="list-group-item" href={itemUrl(item)}>
|
<Link key={itemUrl(item)} role="listitem" className="list-group-item" href={itemUrl(item)}>
|
||||||
<span className="btn-favorite m-r-5">
|
<span className="btn-favorite m-r-5">
|
||||||
<i className="fa fa-star" aria-hidden="true" />
|
<i className="fa fa-star" aria-hidden="true" />
|
||||||
@@ -61,7 +61,7 @@ export function DashboardAndQueryFavoritesList() {
|
|||||||
<FavoriteList
|
<FavoriteList
|
||||||
title="Favorite Dashboards"
|
title="Favorite Dashboards"
|
||||||
resource={Dashboard}
|
resource={Dashboard}
|
||||||
itemUrl={dashboard => dashboard.url}
|
itemUrl={(dashboard) => dashboard.url}
|
||||||
emptyState={
|
emptyState={
|
||||||
<p>
|
<p>
|
||||||
<span className="btn-favorite m-r-5">
|
<span className="btn-favorite m-r-5">
|
||||||
@@ -76,7 +76,7 @@ export function DashboardAndQueryFavoritesList() {
|
|||||||
<FavoriteList
|
<FavoriteList
|
||||||
title="Favorite Queries"
|
title="Favorite Queries"
|
||||||
resource={Query}
|
resource={Query}
|
||||||
itemUrl={query => `queries/${query.id}`}
|
itemUrl={(query) => `queries/${query.id}`}
|
||||||
emptyState={
|
emptyState={
|
||||||
<p>
|
<p>
|
||||||
<span className="btn-favorite m-r-5">
|
<span className="btn-favorite m-r-5">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
import React, { useCallback, useEffect, useRef } from "react";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
|
|
||||||
import routeWithUserSession from "@/components/ApplicationArea/routeWithUserSession";
|
import routeWithUserSession from "@/components/ApplicationArea/routeWithUserSession";
|
||||||
@@ -20,7 +20,7 @@ import ItemsTable, { Columns } from "@/components/items-list/components/ItemsTab
|
|||||||
import Layout from "@/components/layouts/ContentWithSidebar";
|
import Layout from "@/components/layouts/ContentWithSidebar";
|
||||||
|
|
||||||
import { Query } from "@/services/query";
|
import { Query } from "@/services/query";
|
||||||
import { currentUser } from "@/services/auth";
|
import { clientConfig, currentUser } from "@/services/auth";
|
||||||
import location from "@/services/location";
|
import location from "@/services/location";
|
||||||
import routes from "@/services/routes";
|
import routes from "@/services/routes";
|
||||||
|
|
||||||
@@ -95,25 +95,39 @@ function QueriesList({ controller }) {
|
|||||||
const controllerRef = useRef();
|
const controllerRef = useRef();
|
||||||
controllerRef.current = controller;
|
controllerRef.current = controller;
|
||||||
|
|
||||||
|
const updateSearch = useCallback(
|
||||||
|
(searchTemm) => {
|
||||||
|
controller.updateSearch(searchTemm, { isServerSideFTS: !clientConfig.multiByteSearchEnabled });
|
||||||
|
},
|
||||||
|
[controller]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlistenLocationChanges = location.listen((unused, action) => {
|
const unlistenLocationChanges = location.listen((unused, action) => {
|
||||||
const searchTerm = location.search.q || "";
|
const searchTerm = location.search.q || "";
|
||||||
if (action === "PUSH" && searchTerm !== controllerRef.current.searchTerm) {
|
if (action === "PUSH" && searchTerm !== controllerRef.current.searchTerm) {
|
||||||
controllerRef.current.updateSearch(searchTerm);
|
updateSearch(searchTerm);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unlistenLocationChanges();
|
unlistenLocationChanges();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [updateSearch]);
|
||||||
|
|
||||||
|
let usedListColumns = listColumns;
|
||||||
|
if (controller.params.currentPage === "favorites") {
|
||||||
|
usedListColumns = [
|
||||||
|
...usedListColumns,
|
||||||
|
Columns.dateTime.sortable({ title: "Starred At", field: "starred_at", width: "1%" }),
|
||||||
|
];
|
||||||
|
}
|
||||||
const {
|
const {
|
||||||
areExtraActionsAvailable,
|
areExtraActionsAvailable,
|
||||||
listColumns: tableColumns,
|
listColumns: tableColumns,
|
||||||
Component: ExtraActionsComponent,
|
Component: ExtraActionsComponent,
|
||||||
selectedItems,
|
selectedItems,
|
||||||
} = useItemsListExtraActions(controller, listColumns, QueriesListExtraActions);
|
} = useItemsListExtraActions(controller, usedListColumns, QueriesListExtraActions);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-queries-list">
|
<div className="page-queries-list">
|
||||||
@@ -135,7 +149,7 @@ function QueriesList({ controller }) {
|
|||||||
placeholder="Search Queries..."
|
placeholder="Search Queries..."
|
||||||
label="Search queries"
|
label="Search queries"
|
||||||
value={controller.searchTerm}
|
value={controller.searchTerm}
|
||||||
onChange={controller.updateSearch}
|
onChange={updateSearch}
|
||||||
/>
|
/>
|
||||||
<Sidebar.Menu items={sidebarMenu} selected={controller.params.currentPage} />
|
<Sidebar.Menu items={sidebarMenu} selected={controller.params.currentPage} />
|
||||||
<Sidebar.Tags url="api/queries/tags" onChange={controller.updateSelectedTags} showUnselectAll />
|
<Sidebar.Tags url="api/queries/tags" onChange={controller.updateSelectedTags} showUnselectAll />
|
||||||
@@ -160,14 +174,15 @@ function QueriesList({ controller }) {
|
|||||||
orderByField={controller.orderByField}
|
orderByField={controller.orderByField}
|
||||||
orderByReverse={controller.orderByReverse}
|
orderByReverse={controller.orderByReverse}
|
||||||
toggleSorting={controller.toggleSorting}
|
toggleSorting={controller.toggleSorting}
|
||||||
|
setSorting={controller.setSorting}
|
||||||
/>
|
/>
|
||||||
<Paginator
|
<Paginator
|
||||||
showPageSizeSelect
|
showPageSizeSelect
|
||||||
totalCount={controller.totalItemsCount}
|
totalCount={controller.totalItemsCount}
|
||||||
pageSize={controller.itemsPerPage}
|
pageSize={controller.itemsPerPage}
|
||||||
onPageSizeChange={itemsPerPage => controller.updatePagination({ itemsPerPage })}
|
onPageSizeChange={(itemsPerPage) => controller.updatePagination({ itemsPerPage })}
|
||||||
page={controller.page}
|
page={controller.page}
|
||||||
onChange={page => controller.updatePagination({ page })}
|
onChange={(page) => controller.updatePagination({ page })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
@@ -196,10 +211,10 @@ const QueriesListPage = itemsList(
|
|||||||
}[currentPage];
|
}[currentPage];
|
||||||
},
|
},
|
||||||
getItemProcessor() {
|
getItemProcessor() {
|
||||||
return item => new Query(item);
|
return (item) => new Query(item);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
() => new UrlStateStorage({ orderByField: "created_at", orderByReverse: true })
|
({ ...props }) => new UrlStateStorage({ orderByField: props.orderByField ?? "created_at", orderByReverse: true })
|
||||||
);
|
);
|
||||||
|
|
||||||
routes.register(
|
routes.register(
|
||||||
@@ -207,7 +222,7 @@ routes.register(
|
|||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/queries",
|
path: "/queries",
|
||||||
title: "Queries",
|
title: "Queries",
|
||||||
render: pageProps => <QueriesListPage {...pageProps} currentPage="all" />,
|
render: (pageProps) => <QueriesListPage {...pageProps} currentPage="all" />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
routes.register(
|
routes.register(
|
||||||
@@ -215,7 +230,7 @@ routes.register(
|
|||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/queries/favorites",
|
path: "/queries/favorites",
|
||||||
title: "Favorite Queries",
|
title: "Favorite Queries",
|
||||||
render: pageProps => <QueriesListPage {...pageProps} currentPage="favorites" />,
|
render: (pageProps) => <QueriesListPage {...pageProps} currentPage="favorites" orderByField="starred_at" />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
routes.register(
|
routes.register(
|
||||||
@@ -223,7 +238,7 @@ routes.register(
|
|||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/queries/archive",
|
path: "/queries/archive",
|
||||||
title: "Archived Queries",
|
title: "Archived Queries",
|
||||||
render: pageProps => <QueriesListPage {...pageProps} currentPage="archive" />,
|
render: (pageProps) => <QueriesListPage {...pageProps} currentPage="archive" />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
routes.register(
|
routes.register(
|
||||||
@@ -231,6 +246,6 @@ routes.register(
|
|||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/queries/my",
|
path: "/queries/my",
|
||||||
title: "My Queries",
|
title: "My Queries",
|
||||||
render: pageProps => <QueriesListPage {...pageProps} currentPage="my" />,
|
render: (pageProps) => <QueriesListPage {...pageProps} currentPage="my" />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ function normalizeLocation(rawLocation) {
|
|||||||
const result = {};
|
const result = {};
|
||||||
|
|
||||||
result.path = pathname;
|
result.path = pathname;
|
||||||
result.search = mapValues(qs.parse(search), value => (isNil(value) ? true : value));
|
result.search = mapValues(qs.parse(search), (value) => (isNil(value) ? true : value));
|
||||||
result.hash = trimStart(hash, "#");
|
result.hash = trimStart(hash, "#");
|
||||||
result.url = `${pathname}${search}${hash}`;
|
result.url = `${pathname}${search}${hash}`;
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ const location = {
|
|||||||
|
|
||||||
confirmChange(handler) {
|
confirmChange(handler) {
|
||||||
if (isFunction(handler)) {
|
if (isFunction(handler)) {
|
||||||
return history.block(nextLocation => {
|
return history.block((nextLocation) => {
|
||||||
return handler(normalizeLocation(nextLocation), location);
|
return handler(normalizeLocation(nextLocation), location);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -60,12 +60,18 @@ const location = {
|
|||||||
// serialize search and keep existing search parameters (!)
|
// serialize search and keep existing search parameters (!)
|
||||||
if (isObject(newLocation.search)) {
|
if (isObject(newLocation.search)) {
|
||||||
newLocation.search = omitBy(extend({}, location.search, newLocation.search), isNil);
|
newLocation.search = omitBy(extend({}, location.search, newLocation.search), isNil);
|
||||||
newLocation.search = mapValues(newLocation.search, value => (value === true ? null : value));
|
newLocation.search = mapValues(newLocation.search, (value) => (value === true ? null : value));
|
||||||
newLocation.search = qs.stringify(newLocation.search);
|
newLocation.search = qs.stringify(newLocation.search);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (replace) {
|
if (replace) {
|
||||||
history.replace(newLocation);
|
if (
|
||||||
|
newLocation.pathname !== location.path ||
|
||||||
|
newLocation.search !== qs.stringify(location.search) ||
|
||||||
|
newLocation.hash !== location.hash
|
||||||
|
) {
|
||||||
|
history.replace(newLocation);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
history.push(newLocation);
|
history.push(newLocation);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const logger = debug("redash:services:QueryResult");
|
|||||||
const filterTypes = ["filter", "multi-filter", "multiFilter"];
|
const filterTypes = ["filter", "multi-filter", "multiFilter"];
|
||||||
|
|
||||||
function defer() {
|
function defer() {
|
||||||
const result = { onStatusChange: status => {} };
|
const result = { onStatusChange: (status) => {} };
|
||||||
result.promise = new Promise((resolve, reject) => {
|
result.promise = new Promise((resolve, reject) => {
|
||||||
result.resolve = resolve;
|
result.resolve = resolve;
|
||||||
result.reject = reject;
|
result.reject = reject;
|
||||||
@@ -40,13 +40,13 @@ function getColumnNameWithoutType(column) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getColumnFriendlyName(column) {
|
function getColumnFriendlyName(column) {
|
||||||
return getColumnNameWithoutType(column).replace(/(?:^|\s)\S/g, a => a.toUpperCase());
|
return getColumnNameWithoutType(column).replace(/(?:^|\s)\S/g, (a) => a.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
const createOrSaveUrl = data => (data.id ? `api/query_results/${data.id}` : "api/query_results");
|
const createOrSaveUrl = (data) => (data.id ? `api/query_results/${data.id}` : "api/query_results");
|
||||||
const QueryResultResource = {
|
const QueryResultResource = {
|
||||||
get: ({ id }) => axios.get(`api/query_results/${id}`),
|
get: ({ id }) => axios.get(`api/query_results/${id}`),
|
||||||
post: data => axios.post(createOrSaveUrl(data), data),
|
post: (data) => axios.post(createOrSaveUrl(data), data),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExecutionStatus = {
|
export const ExecutionStatus = {
|
||||||
@@ -97,11 +97,11 @@ function handleErrorResponse(queryResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchDataFromJob(jobId, interval = 1000) {
|
export function fetchDataFromJob(jobId, interval = 1000) {
|
||||||
return axios.get(`api/jobs/${jobId}`).then(data => {
|
return axios.get(`api/jobs/${jobId}`).then((data) => {
|
||||||
const status = statuses[data.job.status];
|
const status = statuses[data.job.status];
|
||||||
if (status === ExecutionStatus.WAITING || status === ExecutionStatus.PROCESSING) {
|
if (status === ExecutionStatus.WAITING || status === ExecutionStatus.PROCESSING) {
|
||||||
return sleep(interval).then(() => fetchDataFromJob(data.job.id));
|
return sleep(interval).then(() => fetchDataFromJob(data.job.id));
|
||||||
@@ -146,7 +146,7 @@ class QueryResult {
|
|||||||
// TODO: we should stop manipulating incoming data, and switch to relaying
|
// TODO: we should stop manipulating incoming data, and switch to relaying
|
||||||
// on the column type set by the backend. This logic is prone to errors,
|
// on the column type set by the backend. This logic is prone to errors,
|
||||||
// and better be removed. Kept for now, for backward compatability.
|
// and better be removed. Kept for now, for backward compatability.
|
||||||
each(this.query_result.data.rows, row => {
|
each(this.query_result.data.rows, (row) => {
|
||||||
forOwn(row, (v, k) => {
|
forOwn(row, (v, k) => {
|
||||||
let newType = null;
|
let newType = null;
|
||||||
if (isNumber(v)) {
|
if (isNumber(v)) {
|
||||||
@@ -173,7 +173,7 @@ class QueryResult {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
each(this.query_result.data.columns, column => {
|
each(this.query_result.data.columns, (column) => {
|
||||||
column.name = "" + column.name;
|
column.name = "" + column.name;
|
||||||
if (columnTypes[column.name]) {
|
if (columnTypes[column.name]) {
|
||||||
if (column.type == null || column.type === "string") {
|
if (column.type == null || column.type === "string") {
|
||||||
@@ -265,14 +265,14 @@ class QueryResult {
|
|||||||
|
|
||||||
getColumnNames() {
|
getColumnNames() {
|
||||||
if (this.columnNames === undefined && this.query_result.data) {
|
if (this.columnNames === undefined && this.query_result.data) {
|
||||||
this.columnNames = this.query_result.data.columns.map(v => v.name);
|
this.columnNames = this.query_result.data.columns.map((v) => v.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.columnNames;
|
return this.columnNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
getColumnFriendlyNames() {
|
getColumnFriendlyNames() {
|
||||||
return this.getColumnNames().map(col => getColumnFriendlyName(col));
|
return this.getColumnNames().map((col) => getColumnFriendlyName(col));
|
||||||
}
|
}
|
||||||
|
|
||||||
getTruncated() {
|
getTruncated() {
|
||||||
@@ -286,7 +286,7 @@ class QueryResult {
|
|||||||
|
|
||||||
const filters = [];
|
const filters = [];
|
||||||
|
|
||||||
this.getColumns().forEach(col => {
|
this.getColumns().forEach((col) => {
|
||||||
const name = col.name;
|
const name = col.name;
|
||||||
const type = name.split("::")[1] || name.split("__")[1];
|
const type = name.split("::")[1] || name.split("__")[1];
|
||||||
if (includes(filterTypes, type)) {
|
if (includes(filterTypes, type)) {
|
||||||
@@ -302,8 +302,8 @@ class QueryResult {
|
|||||||
}
|
}
|
||||||
}, this);
|
}, this);
|
||||||
|
|
||||||
this.getRawData().forEach(row => {
|
this.getRawData().forEach((row) => {
|
||||||
filters.forEach(filter => {
|
filters.forEach((filter) => {
|
||||||
filter.values.push(row[filter.name]);
|
filter.values.push(row[filter.name]);
|
||||||
if (filter.values.length === 1) {
|
if (filter.values.length === 1) {
|
||||||
if (filter.multiple) {
|
if (filter.multiple) {
|
||||||
@@ -315,8 +315,8 @@ class QueryResult {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
filters.forEach(filter => {
|
filters.forEach((filter) => {
|
||||||
filter.values = uniqBy(filter.values, v => {
|
filter.values = uniqBy(filter.values, (v) => {
|
||||||
if (moment.isMoment(v)) {
|
if (moment.isMoment(v)) {
|
||||||
return v.unix();
|
return v.unix();
|
||||||
}
|
}
|
||||||
@@ -345,12 +345,12 @@ class QueryResult {
|
|||||||
|
|
||||||
axios
|
axios
|
||||||
.get(`api/queries/${queryId}/results/${id}.json`)
|
.get(`api/queries/${queryId}/results/${id}.json`)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
// Success handler
|
// Success handler
|
||||||
queryResult.isLoadingResult = false;
|
queryResult.isLoadingResult = false;
|
||||||
queryResult.update(response);
|
queryResult.update(response);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
// Error handler
|
// Error handler
|
||||||
queryResult.isLoadingResult = false;
|
queryResult.isLoadingResult = false;
|
||||||
handleErrorResponse(queryResult, error);
|
handleErrorResponse(queryResult, error);
|
||||||
@@ -362,10 +362,10 @@ class QueryResult {
|
|||||||
loadLatestCachedResult(queryId, parameters) {
|
loadLatestCachedResult(queryId, parameters) {
|
||||||
axios
|
axios
|
||||||
.post(`api/queries/${queryId}/results`, { queryId, parameters })
|
.post(`api/queries/${queryId}/results`, { queryId, parameters })
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
this.update(response);
|
this.update(response);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
handleErrorResponse(this, error);
|
handleErrorResponse(this, error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -375,11 +375,11 @@ class QueryResult {
|
|||||||
this.deferred.onStatusChange(ExecutionStatus.LOADING_RESULT);
|
this.deferred.onStatusChange(ExecutionStatus.LOADING_RESULT);
|
||||||
|
|
||||||
QueryResultResource.get({ id: this.job.query_result_id })
|
QueryResultResource.get({ id: this.job.query_result_id })
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
this.update(response);
|
this.update(response);
|
||||||
this.isLoadingResult = false;
|
this.isLoadingResult = false;
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
if (tryCount === undefined) {
|
if (tryCount === undefined) {
|
||||||
tryCount = 0;
|
tryCount = 0;
|
||||||
}
|
}
|
||||||
@@ -394,9 +394,12 @@ class QueryResult {
|
|||||||
});
|
});
|
||||||
this.isLoadingResult = false;
|
this.isLoadingResult = false;
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => {
|
setTimeout(
|
||||||
this.loadResult(tryCount + 1);
|
() => {
|
||||||
}, 1000 * Math.pow(2, tryCount));
|
this.loadResult(tryCount + 1);
|
||||||
|
},
|
||||||
|
1000 * Math.pow(2, tryCount)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -410,19 +413,26 @@ class QueryResult {
|
|||||||
: axios.get(`api/queries/${query}/jobs/${this.job.id}`);
|
: axios.get(`api/queries/${query}/jobs/${this.job.id}`);
|
||||||
|
|
||||||
request
|
request
|
||||||
.then(jobResponse => {
|
.then((jobResponse) => {
|
||||||
this.update(jobResponse);
|
this.update(jobResponse);
|
||||||
|
|
||||||
if (this.getStatus() === "processing" && this.job.query_result_id && this.job.query_result_id !== "None") {
|
if (this.getStatus() === "processing" && this.job.query_result_id && this.job.query_result_id !== "None") {
|
||||||
loadResult();
|
loadResult();
|
||||||
} else if (this.getStatus() !== "failed") {
|
} else if (this.getStatus() !== "failed") {
|
||||||
const waitTime = tryNumber > 10 ? 3000 : 500;
|
let waitTime;
|
||||||
|
if (tryNumber <= 10) {
|
||||||
|
waitTime = 500;
|
||||||
|
} else if (tryNumber <= 50) {
|
||||||
|
waitTime = 1000;
|
||||||
|
} else {
|
||||||
|
waitTime = 3000;
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.refreshStatus(query, parameters, tryNumber + 1);
|
this.refreshStatus(query, parameters, tryNumber + 1);
|
||||||
}, waitTime);
|
}, waitTime);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
logger("Connection error", error);
|
logger("Connection error", error);
|
||||||
// TODO: use QueryResultError, or better yet: exception/reject of promise.
|
// TODO: use QueryResultError, or better yet: exception/reject of promise.
|
||||||
this.update({
|
this.update({
|
||||||
@@ -451,14 +461,14 @@ class QueryResult {
|
|||||||
|
|
||||||
axios
|
axios
|
||||||
.post(`api/queries/${id}/results`, { id, parameters, apply_auto_limit: applyAutoLimit, max_age: maxAge })
|
.post(`api/queries/${id}/results`, { id, parameters, apply_auto_limit: applyAutoLimit, max_age: maxAge })
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
queryResult.update(response);
|
queryResult.update(response);
|
||||||
|
|
||||||
if ("job" in response) {
|
if ("job" in response) {
|
||||||
queryResult.refreshStatus(id, parameters);
|
queryResult.refreshStatus(id, parameters);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
handleErrorResponse(queryResult, error);
|
handleErrorResponse(queryResult, error);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -481,14 +491,14 @@ class QueryResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QueryResultResource.post(params)
|
QueryResultResource.post(params)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
queryResult.update(response);
|
queryResult.update(response);
|
||||||
|
|
||||||
if ("job" in response) {
|
if ("job" in response) {
|
||||||
queryResult.refreshStatus(query, parameters);
|
queryResult.refreshStatus(query, parameters);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
handleErrorResponse(queryResult, error);
|
handleErrorResponse(queryResult, error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ describe("Dashboard", () => {
|
|||||||
cy.getByTestId("DashboardSaveButton").click();
|
cy.getByTestId("DashboardSaveButton").click();
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.wait("@NewDashboard").then(xhr => {
|
cy.wait("@NewDashboard").then((xhr) => {
|
||||||
const id = Cypress._.get(xhr, "response.body.id");
|
const id = Cypress._.get(xhr, "response.body.id");
|
||||||
assert.isDefined(id, "Dashboard api call returns id");
|
assert.isDefined(id, "Dashboard api call returns id");
|
||||||
|
|
||||||
@@ -40,13 +40,9 @@ describe("Dashboard", () => {
|
|||||||
|
|
||||||
cy.getByTestId("DashboardMoreButton").click();
|
cy.getByTestId("DashboardMoreButton").click();
|
||||||
|
|
||||||
cy.getByTestId("DashboardMoreButtonMenu")
|
cy.getByTestId("DashboardMoreButtonMenu").contains("Archive").click();
|
||||||
.contains("Archive")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get(".ant-modal .ant-btn")
|
cy.get(".ant-modal .ant-btn").contains("Archive").click({ force: true });
|
||||||
.contains("Archive")
|
|
||||||
.click({ force: true });
|
|
||||||
cy.get(".label-tag-archived").should("exist");
|
cy.get(".label-tag-archived").should("exist");
|
||||||
|
|
||||||
cy.visit("/dashboards");
|
cy.visit("/dashboards");
|
||||||
@@ -60,7 +56,7 @@ describe("Dashboard", () => {
|
|||||||
cy.server();
|
cy.server();
|
||||||
cy.route("GET", "**/api/dashboards/*").as("LoadDashboard");
|
cy.route("GET", "**/api/dashboards/*").as("LoadDashboard");
|
||||||
cy.createDashboard("Dashboard multiple urls").then(({ id, slug }) => {
|
cy.createDashboard("Dashboard multiple urls").then(({ id, slug }) => {
|
||||||
[`/dashboards/${id}`, `/dashboards/${id}-anything-here`, `/dashboard/${slug}`].forEach(url => {
|
[`/dashboards/${id}`, `/dashboards/${id}-anything-here`, `/dashboard/${slug}`].forEach((url) => {
|
||||||
cy.visit(url);
|
cy.visit(url);
|
||||||
cy.wait("@LoadDashboard");
|
cy.wait("@LoadDashboard");
|
||||||
cy.getByTestId(`DashboardId${id}Container`).should("exist");
|
cy.getByTestId(`DashboardId${id}Container`).should("exist");
|
||||||
@@ -72,7 +68,7 @@ describe("Dashboard", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
context("viewport width is at 800px", () => {
|
context("viewport width is at 800px", () => {
|
||||||
before(function() {
|
before(function () {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.createDashboard("Foo Bar")
|
cy.createDashboard("Foo Bar")
|
||||||
.then(({ id }) => {
|
.then(({ id }) => {
|
||||||
@@ -80,49 +76,42 @@ describe("Dashboard", () => {
|
|||||||
this.dashboardEditUrl = `/dashboards/${id}?edit`;
|
this.dashboardEditUrl = `/dashboards/${id}?edit`;
|
||||||
return cy.addTextbox(id, "Hello World!").then(getWidgetTestId);
|
return cy.addTextbox(id, "Hello World!").then(getWidgetTestId);
|
||||||
})
|
})
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
cy.getByTestId(elTestId).as("textboxEl");
|
cy.getByTestId(elTestId).as("textboxEl");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
cy.viewport(800 + menuWidth, 800);
|
cy.viewport(800 + menuWidth, 800);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows widgets with full width", () => {
|
it("shows widgets with full width", () => {
|
||||||
cy.get("@textboxEl").should($el => {
|
cy.get("@textboxEl").should(($el) => {
|
||||||
expect($el.width()).to.eq(770);
|
expect($el.width()).to.eq(770);
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.viewport(801 + menuWidth, 800);
|
cy.viewport(801 + menuWidth, 800);
|
||||||
cy.get("@textboxEl").should($el => {
|
cy.get("@textboxEl").should(($el) => {
|
||||||
expect($el.width()).to.eq(378);
|
expect($el.width()).to.eq(182);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hides edit option", () => {
|
it("hides edit option", () => {
|
||||||
cy.getByTestId("DashboardMoreButton")
|
cy.getByTestId("DashboardMoreButton").click().should("be.visible");
|
||||||
.click()
|
|
||||||
.should("be.visible");
|
|
||||||
|
|
||||||
cy.getByTestId("DashboardMoreButtonMenu")
|
cy.getByTestId("DashboardMoreButtonMenu").contains("Edit").as("editButton").should("not.be.visible");
|
||||||
.contains("Edit")
|
|
||||||
.as("editButton")
|
|
||||||
.should("not.be.visible");
|
|
||||||
|
|
||||||
cy.viewport(801 + menuWidth, 800);
|
cy.viewport(801 + menuWidth, 800);
|
||||||
cy.get("@editButton").should("be.visible");
|
cy.get("@editButton").should("be.visible");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables edit mode", function() {
|
it("disables edit mode", function () {
|
||||||
cy.viewport(801 + menuWidth, 800);
|
cy.viewport(801 + menuWidth, 800);
|
||||||
cy.visit(this.dashboardEditUrl);
|
cy.visit(this.dashboardEditUrl);
|
||||||
cy.contains("button", "Done Editing")
|
cy.contains("button", "Done Editing").as("saveButton").should("exist");
|
||||||
.as("saveButton")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
cy.viewport(800 + menuWidth, 800);
|
cy.viewport(800 + menuWidth, 800);
|
||||||
cy.contains("button", "Done Editing").should("not.exist");
|
cy.contains("button", "Done Editing").should("not.exist");
|
||||||
@@ -130,14 +119,14 @@ describe("Dashboard", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
context("viewport width is at 767px", () => {
|
context("viewport width is at 767px", () => {
|
||||||
before(function() {
|
before(function () {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.createDashboard("Foo Bar").then(({ id }) => {
|
cy.createDashboard("Foo Bar").then(({ id }) => {
|
||||||
this.dashboardUrl = `/dashboards/${id}`;
|
this.dashboardUrl = `/dashboards/${id}`;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
cy.viewport(767, 800);
|
cy.viewport(767, 800);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { getWidgetTestId, editDashboard, resizeBy } from "../../support/dashboar
|
|||||||
const menuWidth = 80;
|
const menuWidth = 80;
|
||||||
|
|
||||||
describe("Grid compliant widgets", () => {
|
describe("Grid compliant widgets", () => {
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.viewport(1215 + menuWidth, 800);
|
cy.viewport(1215 + menuWidth, 800);
|
||||||
cy.createDashboard("Foo Bar")
|
cy.createDashboard("Foo Bar")
|
||||||
@@ -13,7 +13,7 @@ describe("Grid compliant widgets", () => {
|
|||||||
this.dashboardUrl = `/dashboards/${id}`;
|
this.dashboardUrl = `/dashboards/${id}`;
|
||||||
return cy.addTextbox(id, "Hello World!").then(getWidgetTestId);
|
return cy.addTextbox(id, "Hello World!").then(getWidgetTestId);
|
||||||
})
|
})
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
cy.getByTestId(elTestId).as("textboxEl");
|
cy.getByTestId(elTestId).as("textboxEl");
|
||||||
});
|
});
|
||||||
@@ -27,7 +27,7 @@ describe("Grid compliant widgets", () => {
|
|||||||
|
|
||||||
it("stays put when dragged under snap threshold", () => {
|
it("stays put when dragged under snap threshold", () => {
|
||||||
cy.get("@textboxEl")
|
cy.get("@textboxEl")
|
||||||
.dragBy(90)
|
.dragBy(30)
|
||||||
.invoke("offset")
|
.invoke("offset")
|
||||||
.should("have.property", "left", 15 + menuWidth); // no change, 15 -> 15
|
.should("have.property", "left", 15 + menuWidth); // no change, 15 -> 15
|
||||||
});
|
});
|
||||||
@@ -36,14 +36,14 @@ describe("Grid compliant widgets", () => {
|
|||||||
cy.get("@textboxEl")
|
cy.get("@textboxEl")
|
||||||
.dragBy(110)
|
.dragBy(110)
|
||||||
.invoke("offset")
|
.invoke("offset")
|
||||||
.should("have.property", "left", 215 + menuWidth); // moved by 200, 15 -> 215
|
.should("have.property", "left", 115 + menuWidth); // moved by 100, 15 -> 115
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moves two columns when dragged over snap threshold", () => {
|
it("moves two columns when dragged over snap threshold", () => {
|
||||||
cy.get("@textboxEl")
|
cy.get("@textboxEl")
|
||||||
.dragBy(330)
|
.dragBy(200)
|
||||||
.invoke("offset")
|
.invoke("offset")
|
||||||
.should("have.property", "left", 415 + menuWidth); // moved by 400, 15 -> 415
|
.should("have.property", "left", 215 + menuWidth); // moved by 200, 15 -> 215
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ describe("Grid compliant widgets", () => {
|
|||||||
cy.route("POST", "**/api/widgets/*").as("WidgetSave");
|
cy.route("POST", "**/api/widgets/*").as("WidgetSave");
|
||||||
|
|
||||||
editDashboard();
|
editDashboard();
|
||||||
cy.get("@textboxEl").dragBy(330);
|
cy.get("@textboxEl").dragBy(100);
|
||||||
cy.wait("@WidgetSave");
|
cy.wait("@WidgetSave");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -64,24 +64,24 @@ describe("Grid compliant widgets", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("stays put when dragged under snap threshold", () => {
|
it("stays put when dragged under snap threshold", () => {
|
||||||
resizeBy(cy.get("@textboxEl"), 90)
|
resizeBy(cy.get("@textboxEl"), 30)
|
||||||
.then(() => cy.get("@textboxEl"))
|
.then(() => cy.get("@textboxEl"))
|
||||||
.invoke("width")
|
.invoke("width")
|
||||||
.should("eq", 585); // no change, 585 -> 585
|
.should("eq", 285); // no change, 285 -> 285
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moves one column when dragged over snap threshold", () => {
|
it("moves one column when dragged over snap threshold", () => {
|
||||||
resizeBy(cy.get("@textboxEl"), 110)
|
resizeBy(cy.get("@textboxEl"), 110)
|
||||||
.then(() => cy.get("@textboxEl"))
|
.then(() => cy.get("@textboxEl"))
|
||||||
.invoke("width")
|
.invoke("width")
|
||||||
.should("eq", 785); // resized by 200, 585 -> 785
|
.should("eq", 385); // resized by 200, 185 -> 385
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moves two columns when dragged over snap threshold", () => {
|
it("moves two columns when dragged over snap threshold", () => {
|
||||||
resizeBy(cy.get("@textboxEl"), 400)
|
resizeBy(cy.get("@textboxEl"), 400)
|
||||||
.then(() => cy.get("@textboxEl"))
|
.then(() => cy.get("@textboxEl"))
|
||||||
.invoke("width")
|
.invoke("width")
|
||||||
.should("eq", 985); // resized by 400, 585 -> 985
|
.should("eq", 685); // resized by 400, 285 -> 685
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,16 +101,16 @@ describe("Grid compliant widgets", () => {
|
|||||||
resizeBy(cy.get("@textboxEl"), 0, 30)
|
resizeBy(cy.get("@textboxEl"), 0, 30)
|
||||||
.then(() => cy.get("@textboxEl"))
|
.then(() => cy.get("@textboxEl"))
|
||||||
.invoke("height")
|
.invoke("height")
|
||||||
.should("eq", 185); // resized by 50, , 135 -> 185
|
.should("eq", 185);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shrinks to minimum", () => {
|
it("shrinks to minimum", () => {
|
||||||
cy.get("@textboxEl")
|
cy.get("@textboxEl")
|
||||||
.then($el => resizeBy(cy.get("@textboxEl"), -$el.width(), -$el.height())) // resize to 0,0
|
.then(($el) => resizeBy(cy.get("@textboxEl"), -$el.width(), -$el.height())) // resize to 0,0
|
||||||
.then(() => cy.get("@textboxEl"))
|
.then(() => cy.get("@textboxEl"))
|
||||||
.should($el => {
|
.should(($el) => {
|
||||||
expect($el.width()).to.eq(185); // min textbox width
|
expect($el.width()).to.eq(185); // min textbox width
|
||||||
expect($el.height()).to.eq(35); // min textbox height
|
expect($el.height()).to.eq(85); // min textbox height
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { getWidgetTestId, editDashboard } from "../../support/dashboard";
|
import { getWidgetTestId, editDashboard } from "../../support/dashboard";
|
||||||
|
|
||||||
describe("Textbox", () => {
|
describe("Textbox", () => {
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.createDashboard("Foo Bar").then(({ id }) => {
|
cy.createDashboard("Foo Bar").then(({ id }) => {
|
||||||
this.dashboardId = id;
|
this.dashboardId = id;
|
||||||
@@ -12,12 +12,10 @@ describe("Textbox", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const confirmDeletionInModal = () => {
|
const confirmDeletionInModal = () => {
|
||||||
cy.get(".ant-modal .ant-btn")
|
cy.get(".ant-modal .ant-btn").contains("Delete").click({ force: true });
|
||||||
.contains("Delete")
|
|
||||||
.click({ force: true });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
it("adds textbox", function() {
|
it("adds textbox", function () {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
editDashboard();
|
editDashboard();
|
||||||
cy.getByTestId("AddTextboxButton").click();
|
cy.getByTestId("AddTextboxButton").click();
|
||||||
@@ -29,10 +27,10 @@ describe("Textbox", () => {
|
|||||||
cy.get(".widget-text").should("exist");
|
cy.get(".widget-text").should("exist");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes textbox by X button", function() {
|
it("removes textbox by X button", function () {
|
||||||
cy.addTextbox(this.dashboardId, "Hello World!")
|
cy.addTextbox(this.dashboardId, "Hello World!")
|
||||||
.then(getWidgetTestId)
|
.then(getWidgetTestId)
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
editDashboard();
|
editDashboard();
|
||||||
|
|
||||||
@@ -45,32 +43,30 @@ describe("Textbox", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes textbox by menu", function() {
|
it("removes textbox by menu", function () {
|
||||||
cy.addTextbox(this.dashboardId, "Hello World!")
|
cy.addTextbox(this.dashboardId, "Hello World!")
|
||||||
.then(getWidgetTestId)
|
.then(getWidgetTestId)
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
cy.getByTestId(elTestId).within(() => {
|
cy.getByTestId(elTestId).within(() => {
|
||||||
cy.getByTestId("WidgetDropdownButton").click();
|
cy.getByTestId("WidgetDropdownButton").click();
|
||||||
});
|
});
|
||||||
cy.getByTestId("WidgetDropdownButtonMenu")
|
cy.getByTestId("WidgetDropdownButtonMenu").contains("Remove from Dashboard").click();
|
||||||
.contains("Remove from Dashboard")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
confirmDeletionInModal();
|
confirmDeletionInModal();
|
||||||
cy.getByTestId(elTestId).should("not.exist");
|
cy.getByTestId(elTestId).should("not.exist");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows opening menu after removal", function() {
|
it("allows opening menu after removal", function () {
|
||||||
let elTestId1;
|
let elTestId1;
|
||||||
cy.addTextbox(this.dashboardId, "txb 1")
|
cy.addTextbox(this.dashboardId, "txb 1")
|
||||||
.then(getWidgetTestId)
|
.then(getWidgetTestId)
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
elTestId1 = elTestId;
|
elTestId1 = elTestId;
|
||||||
return cy.addTextbox(this.dashboardId, "txb 2").then(getWidgetTestId);
|
return cy.addTextbox(this.dashboardId, "txb 2").then(getWidgetTestId);
|
||||||
})
|
})
|
||||||
.then(elTestId2 => {
|
.then((elTestId2) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
editDashboard();
|
editDashboard();
|
||||||
|
|
||||||
@@ -97,10 +93,10 @@ describe("Textbox", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("edits textbox", function() {
|
it("edits textbox", function () {
|
||||||
cy.addTextbox(this.dashboardId, "Hello World!")
|
cy.addTextbox(this.dashboardId, "Hello World!")
|
||||||
.then(getWidgetTestId)
|
.then(getWidgetTestId)
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
cy.getByTestId(elTestId)
|
cy.getByTestId(elTestId)
|
||||||
.as("textboxEl")
|
.as("textboxEl")
|
||||||
@@ -108,17 +104,13 @@ describe("Textbox", () => {
|
|||||||
cy.getByTestId("WidgetDropdownButton").click();
|
cy.getByTestId("WidgetDropdownButton").click();
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.getByTestId("WidgetDropdownButtonMenu")
|
cy.getByTestId("WidgetDropdownButtonMenu").contains("Edit").click();
|
||||||
.contains("Edit")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
const newContent = "[edited]";
|
const newContent = "[edited]";
|
||||||
cy.getByTestId("TextboxDialog")
|
cy.getByTestId("TextboxDialog")
|
||||||
.should("exist")
|
.should("exist")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get("textarea")
|
cy.get("textarea").clear().type(newContent);
|
||||||
.clear()
|
|
||||||
.type(newContent);
|
|
||||||
cy.contains("button", "Save").click();
|
cy.contains("button", "Save").click();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -126,7 +118,7 @@ describe("Textbox", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders textbox according to position configuration", function() {
|
it("renders textbox according to position configuration", function () {
|
||||||
const id = this.dashboardId;
|
const id = this.dashboardId;
|
||||||
const txb1Pos = { col: 0, row: 0, sizeX: 3, sizeY: 2 };
|
const txb1Pos = { col: 0, row: 0, sizeX: 3, sizeY: 2 };
|
||||||
const txb2Pos = { col: 1, row: 1, sizeX: 3, sizeY: 4 };
|
const txb2Pos = { col: 1, row: 1, sizeX: 3, sizeY: 4 };
|
||||||
@@ -135,15 +127,15 @@ describe("Textbox", () => {
|
|||||||
cy.addTextbox(id, "x", { position: txb1Pos })
|
cy.addTextbox(id, "x", { position: txb1Pos })
|
||||||
.then(() => cy.addTextbox(id, "x", { position: txb2Pos }))
|
.then(() => cy.addTextbox(id, "x", { position: txb2Pos }))
|
||||||
.then(getWidgetTestId)
|
.then(getWidgetTestId)
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
return cy.getByTestId(elTestId);
|
return cy.getByTestId(elTestId);
|
||||||
})
|
})
|
||||||
.should($el => {
|
.should(($el) => {
|
||||||
const { top, left } = $el.offset();
|
const { top, left } = $el.offset();
|
||||||
expect(top).to.be.oneOf([162, 162.015625]);
|
expect(top).to.be.oneOf([162, 162.015625]);
|
||||||
expect(left).to.eq(282);
|
expect(left).to.eq(188);
|
||||||
expect($el.width()).to.eq(545);
|
expect($el.width()).to.eq(265);
|
||||||
expect($el.height()).to.eq(185);
|
expect($el.height()).to.eq(185);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
26
migrations/versions/1655999df5e3_default_alert_selector.py
Normal file
26
migrations/versions/1655999df5e3_default_alert_selector.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"""set default alert selector
|
||||||
|
|
||||||
|
Revision ID: 1655999df5e3
|
||||||
|
Revises: 9e8c841d1a30
|
||||||
|
Create Date: 2025-07-09 14:44:00
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '1655999df5e3'
|
||||||
|
down_revision = '9e8c841d1a30'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.execute("""
|
||||||
|
UPDATE alerts
|
||||||
|
SET options = jsonb_set(options, '{selector}', '"first"')
|
||||||
|
WHERE options->>'selector' IS NULL;
|
||||||
|
""")
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
pass
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""12-column dashboard layout
|
||||||
|
|
||||||
|
Revision ID: db0aca1ebd32
|
||||||
|
Revises: 1655999df5e3
|
||||||
|
Create Date: 2025-03-31 13:45:43.160893
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'db0aca1ebd32'
|
||||||
|
down_revision = '1655999df5e3'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.execute("""
|
||||||
|
UPDATE widgets
|
||||||
|
SET options = jsonb_set(options, '{position,col}', to_json((options->'position'->>'col')::int * 2)::jsonb);
|
||||||
|
UPDATE widgets
|
||||||
|
SET options = jsonb_set(options, '{position,sizeX}', to_json((options->'position'->>'sizeX')::int * 2)::jsonb);
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.execute("""
|
||||||
|
UPDATE widgets
|
||||||
|
SET options = jsonb_set(options, '{position,col}', to_json((options->'position'->>'col')::int / 2)::jsonb);
|
||||||
|
UPDATE widgets
|
||||||
|
SET options = jsonb_set(options, '{position,sizeX}', to_json((options->'position'->>'sizeX')::int / 2)::jsonb);
|
||||||
|
""")
|
||||||
19
package.json
19
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "redash-client",
|
"name": "redash-client",
|
||||||
"version": "25.03.0-dev",
|
"version": "25.10.0-dev",
|
||||||
"description": "The frontend part of Redash.",
|
"description": "The frontend part of Redash.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
"@ant-design/icons": "^4.2.1",
|
"@ant-design/icons": "^4.2.1",
|
||||||
"@redash/viz": "file:viz-lib",
|
"@redash/viz": "file:viz-lib",
|
||||||
"ace-builds": "^1.4.12",
|
"ace-builds": "^1.4.12",
|
||||||
"antd": "^4.4.3",
|
"antd": "4.4.3",
|
||||||
"axios": "0.27.2",
|
"axios": "0.27.2",
|
||||||
"axios-auth-refresh": "3.3.6",
|
"axios-auth-refresh": "3.3.6",
|
||||||
"bootstrap": "^3.4.1",
|
"bootstrap": "^3.4.1",
|
||||||
@@ -100,6 +100,7 @@
|
|||||||
"@types/sql-formatter": "^2.3.0",
|
"@types/sql-formatter": "^2.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.10.0",
|
"@typescript-eslint/eslint-plugin": "^2.10.0",
|
||||||
"@typescript-eslint/parser": "^2.10.0",
|
"@typescript-eslint/parser": "^2.10.0",
|
||||||
|
"assert": "^2.1.0",
|
||||||
"atob": "^2.1.2",
|
"atob": "^2.1.2",
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.0.3",
|
||||||
"babel-jest": "^24.1.0",
|
"babel-jest": "^24.1.0",
|
||||||
@@ -138,20 +139,24 @@
|
|||||||
"mini-css-extract-plugin": "^1.6.2",
|
"mini-css-extract-plugin": "^1.6.2",
|
||||||
"mockdate": "^2.0.2",
|
"mockdate": "^2.0.2",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "3.3.2",
|
||||||
|
"process": "^0.11.10",
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
"react-refresh": "^0.14.0",
|
"react-refresh": "^0.14.0",
|
||||||
"react-test-renderer": "^16.14.0",
|
"react-test-renderer": "^16.14.0",
|
||||||
"request-cookies": "^1.1.0",
|
"request-cookies": "^1.1.0",
|
||||||
|
"source-map-loader": "^1.1.3",
|
||||||
|
"stream-browserify": "^3.0.0",
|
||||||
"style-loader": "^2.0.0",
|
"style-loader": "^2.0.0",
|
||||||
"typescript": "^4.1.2",
|
"typescript": "4.1.2",
|
||||||
|
"url": "^0.11.4",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"webpack": "^4.46.0",
|
"webpack": "^5.101.3",
|
||||||
"webpack-build-notifier": "^2.3.0",
|
"webpack-build-notifier": "^3.0.1",
|
||||||
"webpack-bundle-analyzer": "^4.9.0",
|
"webpack-bundle-analyzer": "^4.9.0",
|
||||||
"webpack-cli": "^4.10.0",
|
"webpack-cli": "^4.10.0",
|
||||||
"webpack-dev-server": "^4.15.1",
|
"webpack-dev-server": "^4.15.1",
|
||||||
"webpack-manifest-plugin": "^2.0.4"
|
"webpack-manifest-plugin": "^5.0.1"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"fsevents": "^2.3.2"
|
"fsevents": "^2.3.2"
|
||||||
|
|||||||
3045
poetry.lock
generated
3045
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,17 @@
|
|||||||
[project]
|
[project]
|
||||||
|
name = "redash"
|
||||||
|
version = "25.10.0-dev"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
|
description = "Make Your Company Data Driven. Connect to any data source, easily visualize, dashboard and share your data."
|
||||||
|
authors = [
|
||||||
|
{ name = "Arik Fraimovich", email = "<arik@redash.io>" }
|
||||||
|
]
|
||||||
|
# to be added to/removed from the mailing list, please reach out to Arik via the above email or Discord
|
||||||
|
maintainers = [
|
||||||
|
{ name = "Redash maintainers and contributors", email = "<maintainers@redash.io>" }
|
||||||
|
]
|
||||||
|
readme = "README.md"
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
target-version = ['py38']
|
target-version = ['py38']
|
||||||
@@ -10,17 +22,6 @@ force-exclude = '''
|
|||||||
)/
|
)/
|
||||||
'''
|
'''
|
||||||
|
|
||||||
[tool.poetry]
|
|
||||||
name = "redash"
|
|
||||||
version = "25.03.0-dev"
|
|
||||||
description = "Make Your Company Data Driven. Connect to any data source, easily visualize, dashboard and share your data."
|
|
||||||
authors = ["Arik Fraimovich <arik@redash.io>"]
|
|
||||||
# to be added to/removed from the mailing list, please reach out to Arik via the above email or Discord
|
|
||||||
maintainers = [
|
|
||||||
"Redash maintainers and contributors <maintainers@redash.io>",
|
|
||||||
]
|
|
||||||
readme = "README.md"
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.8,<3.11"
|
python = ">=3.8,<3.11"
|
||||||
advocate = "1.0.0"
|
advocate = "1.0.0"
|
||||||
@@ -95,7 +96,7 @@ optional = true
|
|||||||
|
|
||||||
[tool.poetry.group.all_ds.dependencies]
|
[tool.poetry.group.all_ds.dependencies]
|
||||||
atsd-client = "3.0.5"
|
atsd-client = "3.0.5"
|
||||||
azure-kusto-data = "0.0.35"
|
azure-kusto-data = "5.0.1"
|
||||||
boto3 = "1.28.8"
|
boto3 = "1.28.8"
|
||||||
botocore = "1.31.8"
|
botocore = "1.31.8"
|
||||||
cassandra-driver = "3.21.0"
|
cassandra-driver = "3.21.0"
|
||||||
@@ -110,6 +111,7 @@ influxdb = "5.2.3"
|
|||||||
influxdb-client = "1.38.0"
|
influxdb-client = "1.38.0"
|
||||||
memsql = "3.2.0"
|
memsql = "3.2.0"
|
||||||
mysqlclient = "2.1.1"
|
mysqlclient = "2.1.1"
|
||||||
|
numpy = "1.24.4"
|
||||||
nzalchemy = "^11.0.2"
|
nzalchemy = "^11.0.2"
|
||||||
nzpy = ">=1.15"
|
nzpy = ">=1.15"
|
||||||
oauth2client = "4.1.3"
|
oauth2client = "4.1.3"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from redash.app import create_app # noqa
|
|||||||
from redash.destinations import import_destinations
|
from redash.destinations import import_destinations
|
||||||
from redash.query_runner import import_query_runners
|
from redash.query_runner import import_query_runners
|
||||||
|
|
||||||
__version__ = "25.03.0-dev"
|
__version__ = "25.10.0-dev"
|
||||||
|
|
||||||
|
|
||||||
if os.environ.get("REMOTE_DEBUG"):
|
if os.environ.get("REMOTE_DEBUG"):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import requests
|
|||||||
from authlib.integrations.flask_client import OAuth
|
from authlib.integrations.flask_client import OAuth
|
||||||
from flask import Blueprint, flash, redirect, request, session, url_for
|
from flask import Blueprint, flash, redirect, request, session, url_for
|
||||||
|
|
||||||
from redash import models
|
from redash import models, settings
|
||||||
from redash.authentication import (
|
from redash.authentication import (
|
||||||
create_and_login_user,
|
create_and_login_user,
|
||||||
get_next_path,
|
get_next_path,
|
||||||
@@ -29,6 +29,41 @@ def verify_profile(org, profile):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_profile(access_token, logger):
|
||||||
|
headers = {"Authorization": f"OAuth {access_token}"}
|
||||||
|
response = requests.get("https://www.googleapis.com/oauth2/v1/userinfo", headers=headers)
|
||||||
|
|
||||||
|
if response.status_code == 401:
|
||||||
|
logger.warning("Failed getting user profile (response code 401).")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def build_redirect_uri():
|
||||||
|
scheme = settings.GOOGLE_OAUTH_SCHEME_OVERRIDE or None
|
||||||
|
return url_for(".callback", _external=True, _scheme=scheme)
|
||||||
|
|
||||||
|
|
||||||
|
def build_next_path(org_slug=None):
|
||||||
|
next_path = request.args.get("next")
|
||||||
|
if not next_path:
|
||||||
|
if org_slug is None:
|
||||||
|
org_slug = session.get("org_slug")
|
||||||
|
|
||||||
|
scheme = None
|
||||||
|
if settings.GOOGLE_OAUTH_SCHEME_OVERRIDE:
|
||||||
|
scheme = settings.GOOGLE_OAUTH_SCHEME_OVERRIDE
|
||||||
|
|
||||||
|
next_path = url_for(
|
||||||
|
"redash.index",
|
||||||
|
org_slug=org_slug,
|
||||||
|
_external=True,
|
||||||
|
_scheme=scheme,
|
||||||
|
)
|
||||||
|
return next_path
|
||||||
|
|
||||||
|
|
||||||
def create_google_oauth_blueprint(app):
|
def create_google_oauth_blueprint(app):
|
||||||
oauth = OAuth(app)
|
oauth = OAuth(app)
|
||||||
|
|
||||||
@@ -36,23 +71,12 @@ def create_google_oauth_blueprint(app):
|
|||||||
blueprint = Blueprint("google_oauth", __name__)
|
blueprint = Blueprint("google_oauth", __name__)
|
||||||
|
|
||||||
CONF_URL = "https://accounts.google.com/.well-known/openid-configuration"
|
CONF_URL = "https://accounts.google.com/.well-known/openid-configuration"
|
||||||
oauth = OAuth(app)
|
|
||||||
oauth.register(
|
oauth.register(
|
||||||
name="google",
|
name="google",
|
||||||
server_metadata_url=CONF_URL,
|
server_metadata_url=CONF_URL,
|
||||||
client_kwargs={"scope": "openid email profile"},
|
client_kwargs={"scope": "openid email profile"},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_user_profile(access_token):
|
|
||||||
headers = {"Authorization": "OAuth {}".format(access_token)}
|
|
||||||
response = requests.get("https://www.googleapis.com/oauth2/v1/userinfo", headers=headers)
|
|
||||||
|
|
||||||
if response.status_code == 401:
|
|
||||||
logger.warning("Failed getting user profile (response code 401).")
|
|
||||||
return None
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
@blueprint.route("/<org_slug>/oauth/google", endpoint="authorize_org")
|
@blueprint.route("/<org_slug>/oauth/google", endpoint="authorize_org")
|
||||||
def org_login(org_slug):
|
def org_login(org_slug):
|
||||||
session["org_slug"] = current_org.slug
|
session["org_slug"] = current_org.slug
|
||||||
@@ -60,9 +84,9 @@ def create_google_oauth_blueprint(app):
|
|||||||
|
|
||||||
@blueprint.route("/oauth/google", endpoint="authorize")
|
@blueprint.route("/oauth/google", endpoint="authorize")
|
||||||
def login():
|
def login():
|
||||||
redirect_uri = url_for(".callback", _external=True)
|
redirect_uri = build_redirect_uri()
|
||||||
|
|
||||||
next_path = request.args.get("next", url_for("redash.index", org_slug=session.get("org_slug")))
|
next_path = build_next_path()
|
||||||
logger.debug("Callback url: %s", redirect_uri)
|
logger.debug("Callback url: %s", redirect_uri)
|
||||||
logger.debug("Next is: %s", next_path)
|
logger.debug("Next is: %s", next_path)
|
||||||
|
|
||||||
@@ -86,7 +110,7 @@ def create_google_oauth_blueprint(app):
|
|||||||
flash("Validation error. Please retry.")
|
flash("Validation error. Please retry.")
|
||||||
return redirect(url_for("redash.login"))
|
return redirect(url_for("redash.login"))
|
||||||
|
|
||||||
profile = get_user_profile(access_token)
|
profile = get_user_profile(access_token, logger)
|
||||||
if profile is None:
|
if profile is None:
|
||||||
flash("Validation error. Please retry.")
|
flash("Validation error. Please retry.")
|
||||||
return redirect(url_for("redash.login"))
|
return redirect(url_for("redash.login"))
|
||||||
@@ -110,7 +134,9 @@ def create_google_oauth_blueprint(app):
|
|||||||
if user is None:
|
if user is None:
|
||||||
return logout_and_redirect_to_index()
|
return logout_and_redirect_to_index()
|
||||||
|
|
||||||
unsafe_next_path = session.get("next_url") or url_for("redash.index", org_slug=org.slug)
|
unsafe_next_path = session.get("next_url")
|
||||||
|
if not unsafe_next_path:
|
||||||
|
unsafe_next_path = build_next_path(org.slug)
|
||||||
next_path = get_next_path(unsafe_next_path)
|
next_path = get_next_path(unsafe_next_path)
|
||||||
|
|
||||||
return redirect(next_path)
|
return redirect(next_path)
|
||||||
|
|||||||
@@ -255,6 +255,12 @@ def number_format_config():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def null_value_config():
|
||||||
|
return {
|
||||||
|
"nullValue": current_org.get_setting("null_value"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def client_config():
|
def client_config():
|
||||||
if not current_user.is_api_user() and current_user.is_authenticated:
|
if not current_user.is_api_user() and current_user.is_authenticated:
|
||||||
client_config = {
|
client_config = {
|
||||||
@@ -272,6 +278,7 @@ def client_config():
|
|||||||
"showPermissionsControl": current_org.get_setting("feature_show_permissions_control"),
|
"showPermissionsControl": current_org.get_setting("feature_show_permissions_control"),
|
||||||
"hidePlotlyModeBar": current_org.get_setting("hide_plotly_mode_bar"),
|
"hidePlotlyModeBar": current_org.get_setting("hide_plotly_mode_bar"),
|
||||||
"disablePublicUrls": current_org.get_setting("disable_public_urls"),
|
"disablePublicUrls": current_org.get_setting("disable_public_urls"),
|
||||||
|
"multiByteSearchEnabled": current_org.get_setting("multi_byte_search_enabled"),
|
||||||
"allowCustomJSVisualizations": settings.FEATURE_ALLOW_CUSTOM_JS_VISUALIZATIONS,
|
"allowCustomJSVisualizations": settings.FEATURE_ALLOW_CUSTOM_JS_VISUALIZATIONS,
|
||||||
"autoPublishNamedQueries": settings.FEATURE_AUTO_PUBLISH_NAMED_QUERIES,
|
"autoPublishNamedQueries": settings.FEATURE_AUTO_PUBLISH_NAMED_QUERIES,
|
||||||
"extendedAlertOptions": settings.FEATURE_EXTENDED_ALERT_OPTIONS,
|
"extendedAlertOptions": settings.FEATURE_EXTENDED_ALERT_OPTIONS,
|
||||||
@@ -289,6 +296,7 @@ def client_config():
|
|||||||
client_config.update({"basePath": base_href()})
|
client_config.update({"basePath": base_href()})
|
||||||
client_config.update(date_time_format_config())
|
client_config.update(date_time_format_config())
|
||||||
client_config.update(number_format_config())
|
client_config.update(number_format_config())
|
||||||
|
client_config.update(null_value_config())
|
||||||
|
|
||||||
return client_config
|
return client_config
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ order_map = {
|
|||||||
"-name": "-lowercase_name",
|
"-name": "-lowercase_name",
|
||||||
"created_at": "created_at",
|
"created_at": "created_at",
|
||||||
"-created_at": "-created_at",
|
"-created_at": "-created_at",
|
||||||
|
"starred_at": "favorites-created_at",
|
||||||
|
"-starred_at": "-favorites-created_at",
|
||||||
}
|
}
|
||||||
|
|
||||||
order_results = partial(_order_results, default_order="-created_at", allowed_orders=order_map)
|
order_results = partial(_order_results, default_order="-created_at", allowed_orders=order_map)
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ order_map = {
|
|||||||
"-executed_at": "-query_results-retrieved_at",
|
"-executed_at": "-query_results-retrieved_at",
|
||||||
"created_by": "users-name",
|
"created_by": "users-name",
|
||||||
"-created_by": "-users-name",
|
"-created_by": "-users-name",
|
||||||
|
"starred_at": "favorites-created_at",
|
||||||
|
"-starred_at": "-favorites-created_at",
|
||||||
}
|
}
|
||||||
|
|
||||||
order_results = partial(_order_results, default_order="-created_at", allowed_orders=order_map)
|
order_results = partial(_order_results, default_order="-created_at", allowed_orders=order_map)
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ class DataSource(BelongsToOrgMixin, db.Model):
|
|||||||
|
|
||||||
def _sort_schema(self, schema):
|
def _sort_schema(self, schema):
|
||||||
return [
|
return [
|
||||||
{"name": i["name"], "columns": sorted(i["columns"], key=lambda x: x["name"] if isinstance(x, dict) else x)}
|
{**i, "columns": sorted(i["columns"], key=lambda x: x["name"] if isinstance(x, dict) else x)}
|
||||||
for i in sorted(schema, key=lambda x: x["name"])
|
for i in sorted(schema, key=lambda x: x["name"])
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -564,7 +564,7 @@ class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
|
|||||||
db.session.query(tag_column, usage_count)
|
db.session.query(tag_column, usage_count)
|
||||||
.group_by(tag_column)
|
.group_by(tag_column)
|
||||||
.filter(Query.id.in_(queries.options(load_only("id"))))
|
.filter(Query.id.in_(queries.options(load_only("id"))))
|
||||||
.order_by(usage_count.desc())
|
.order_by(tag_column)
|
||||||
)
|
)
|
||||||
return query
|
return query
|
||||||
|
|
||||||
@@ -1137,7 +1137,7 @@ class Dashboard(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model
|
|||||||
db.session.query(tag_column, usage_count)
|
db.session.query(tag_column, usage_count)
|
||||||
.group_by(tag_column)
|
.group_by(tag_column)
|
||||||
.filter(Dashboard.id.in_(dashboards.options(load_only("id"))))
|
.filter(Dashboard.id.in_(dashboards.options(load_only("id"))))
|
||||||
.order_by(usage_count.desc())
|
.order_by(tag_column)
|
||||||
)
|
)
|
||||||
return query
|
return query
|
||||||
|
|
||||||
@@ -1145,15 +1145,19 @@ class Dashboard(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model
|
|||||||
def favorites(cls, user, base_query=None):
|
def favorites(cls, user, base_query=None):
|
||||||
if base_query is None:
|
if base_query is None:
|
||||||
base_query = cls.all(user.org, user.group_ids, user.id)
|
base_query = cls.all(user.org, user.group_ids, user.id)
|
||||||
return base_query.join(
|
return (
|
||||||
(
|
base_query.distinct(cls.lowercase_name, Dashboard.created_at, Dashboard.slug, Favorite.created_at)
|
||||||
Favorite,
|
.join(
|
||||||
and_(
|
(
|
||||||
Favorite.object_type == "Dashboard",
|
Favorite,
|
||||||
Favorite.object_id == Dashboard.id,
|
and_(
|
||||||
),
|
Favorite.object_type == "Dashboard",
|
||||||
|
Favorite.object_id == Dashboard.id,
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
).filter(Favorite.user_id == user.id)
|
.filter(Favorite.user_id == user.id)
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def by_user(cls, user):
|
def by_user(cls, user):
|
||||||
|
|||||||
@@ -288,7 +288,10 @@ class BaseSQLQueryRunner(BaseQueryRunner):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def query_is_select_no_limit(self, query):
|
def query_is_select_no_limit(self, query):
|
||||||
parsed_query = sqlparse.parse(query)[0]
|
parsed_query_list = sqlparse.parse(query)
|
||||||
|
if len(parsed_query_list) == 0:
|
||||||
|
return False
|
||||||
|
parsed_query = parsed_query_list[0]
|
||||||
last_keyword_idx = find_last_keyword_idx(parsed_query)
|
last_keyword_idx = find_last_keyword_idx(parsed_query)
|
||||||
# Either invalid query or query that is not select
|
# Either invalid query or query that is not select
|
||||||
if last_keyword_idx == -1 or parsed_query.tokens[0].value.upper() != "SELECT":
|
if last_keyword_idx == -1 or parsed_query.tokens[0].value.upper() != "SELECT":
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ from redash.query_runner import (
|
|||||||
from redash.utils import json_loads
|
from redash.utils import json_loads
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from azure.kusto.data.exceptions import KustoServiceError
|
from azure.kusto.data import (
|
||||||
from azure.kusto.data.request import (
|
|
||||||
ClientRequestProperties,
|
ClientRequestProperties,
|
||||||
KustoClient,
|
KustoClient,
|
||||||
KustoConnectionStringBuilder,
|
KustoConnectionStringBuilder,
|
||||||
)
|
)
|
||||||
|
from azure.kusto.data.exceptions import KustoServiceError
|
||||||
|
|
||||||
enabled = True
|
enabled = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -37,6 +37,34 @@ TYPES_MAP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_data_scanned(kusto_response):
|
||||||
|
try:
|
||||||
|
metadata_table = next(
|
||||||
|
(table for table in kusto_response.tables if table.table_name == "QueryCompletionInformation"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if metadata_table:
|
||||||
|
resource_usage_json = next(
|
||||||
|
(row["Payload"] for row in metadata_table.rows if row["EventTypeName"] == "QueryResourceConsumption"),
|
||||||
|
"{}",
|
||||||
|
)
|
||||||
|
resource_usage = json_loads(resource_usage_json).get("resource_usage", {})
|
||||||
|
|
||||||
|
data_scanned = (
|
||||||
|
resource_usage["cache"]["shards"]["cold"]["hitbytes"]
|
||||||
|
+ resource_usage["cache"]["shards"]["cold"]["missbytes"]
|
||||||
|
+ resource_usage["cache"]["shards"]["hot"]["hitbytes"]
|
||||||
|
+ resource_usage["cache"]["shards"]["hot"]["missbytes"]
|
||||||
|
+ resource_usage["cache"]["shards"]["bypassbytes"]
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
data_scanned = 0
|
||||||
|
|
||||||
|
return int(data_scanned)
|
||||||
|
|
||||||
|
|
||||||
class AzureKusto(BaseQueryRunner):
|
class AzureKusto(BaseQueryRunner):
|
||||||
should_annotate_query = False
|
should_annotate_query = False
|
||||||
noop_query = "let noop = datatable (Noop:string)[1]; noop"
|
noop_query = "let noop = datatable (Noop:string)[1]; noop"
|
||||||
@@ -44,8 +72,6 @@ class AzureKusto(BaseQueryRunner):
|
|||||||
def __init__(self, configuration):
|
def __init__(self, configuration):
|
||||||
super(AzureKusto, self).__init__(configuration)
|
super(AzureKusto, self).__init__(configuration)
|
||||||
self.syntax = "custom"
|
self.syntax = "custom"
|
||||||
self.client_request_properties = ClientRequestProperties()
|
|
||||||
self.client_request_properties.application = "redash"
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def configuration_schema(cls):
|
def configuration_schema(cls):
|
||||||
@@ -60,12 +86,14 @@ class AzureKusto(BaseQueryRunner):
|
|||||||
},
|
},
|
||||||
"azure_ad_tenant_id": {"type": "string", "title": "Azure AD Tenant Id"},
|
"azure_ad_tenant_id": {"type": "string", "title": "Azure AD Tenant Id"},
|
||||||
"database": {"type": "string"},
|
"database": {"type": "string"},
|
||||||
|
"msi": {"type": "boolean", "title": "Use Managed Service Identity"},
|
||||||
|
"user_msi": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "User-assigned managed identity client ID",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"cluster",
|
"cluster",
|
||||||
"azure_ad_client_id",
|
|
||||||
"azure_ad_client_secret",
|
|
||||||
"azure_ad_tenant_id",
|
|
||||||
"database",
|
"database",
|
||||||
],
|
],
|
||||||
"order": [
|
"order": [
|
||||||
@@ -91,18 +119,48 @@ class AzureKusto(BaseQueryRunner):
|
|||||||
return "Azure Data Explorer (Kusto)"
|
return "Azure Data Explorer (Kusto)"
|
||||||
|
|
||||||
def run_query(self, query, user):
|
def run_query(self, query, user):
|
||||||
kcsb = KustoConnectionStringBuilder.with_aad_application_key_authentication(
|
cluster = self.configuration["cluster"]
|
||||||
connection_string=self.configuration["cluster"],
|
msi = self.configuration.get("msi", False)
|
||||||
aad_app_id=self.configuration["azure_ad_client_id"],
|
# Managed Service Identity(MSI)
|
||||||
app_key=self.configuration["azure_ad_client_secret"],
|
if msi:
|
||||||
authority_id=self.configuration["azure_ad_tenant_id"],
|
# If user-assigned managed identity is used, the client ID must be provided
|
||||||
)
|
if self.configuration.get("user_msi"):
|
||||||
|
kcsb = KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication(
|
||||||
|
cluster,
|
||||||
|
client_id=self.configuration["user_msi"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
kcsb = KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication(cluster)
|
||||||
|
# Service Principal auth
|
||||||
|
else:
|
||||||
|
aad_app_id = self.configuration.get("azure_ad_client_id")
|
||||||
|
app_key = self.configuration.get("azure_ad_client_secret")
|
||||||
|
authority_id = self.configuration.get("azure_ad_tenant_id")
|
||||||
|
|
||||||
|
if not (aad_app_id and app_key and authority_id):
|
||||||
|
raise ValueError(
|
||||||
|
"Azure AD Client ID, Client Secret, and Tenant ID are required for Service Principal authentication."
|
||||||
|
)
|
||||||
|
|
||||||
|
kcsb = KustoConnectionStringBuilder.with_aad_application_key_authentication(
|
||||||
|
connection_string=cluster,
|
||||||
|
aad_app_id=aad_app_id,
|
||||||
|
app_key=app_key,
|
||||||
|
authority_id=authority_id,
|
||||||
|
)
|
||||||
|
|
||||||
client = KustoClient(kcsb)
|
client = KustoClient(kcsb)
|
||||||
|
|
||||||
|
request_properties = ClientRequestProperties()
|
||||||
|
request_properties.application = "redash"
|
||||||
|
|
||||||
|
if user:
|
||||||
|
request_properties.user = user.email
|
||||||
|
request_properties.set_option("request_description", user.email)
|
||||||
|
|
||||||
db = self.configuration["database"]
|
db = self.configuration["database"]
|
||||||
try:
|
try:
|
||||||
response = client.execute(db, query, self.client_request_properties)
|
response = client.execute(db, query, request_properties)
|
||||||
|
|
||||||
result_cols = response.primary_results[0].columns
|
result_cols = response.primary_results[0].columns
|
||||||
result_rows = response.primary_results[0].rows
|
result_rows = response.primary_results[0].rows
|
||||||
@@ -123,14 +181,15 @@ class AzureKusto(BaseQueryRunner):
|
|||||||
rows.append(row.to_dict())
|
rows.append(row.to_dict())
|
||||||
|
|
||||||
error = None
|
error = None
|
||||||
data = {"columns": columns, "rows": rows}
|
data = {
|
||||||
|
"columns": columns,
|
||||||
|
"rows": rows,
|
||||||
|
"metadata": {"data_scanned": _get_data_scanned(response)},
|
||||||
|
}
|
||||||
|
|
||||||
except KustoServiceError as err:
|
except KustoServiceError as err:
|
||||||
data = None
|
data = None
|
||||||
try:
|
error = str(err)
|
||||||
error = err.args[1][0]["error"]["@message"]
|
|
||||||
except (IndexError, KeyError):
|
|
||||||
error = err.args[1]
|
|
||||||
|
|
||||||
return data, error
|
return data, error
|
||||||
|
|
||||||
@@ -143,7 +202,10 @@ class AzureKusto(BaseQueryRunner):
|
|||||||
self._handle_run_query_error(error)
|
self._handle_run_query_error(error)
|
||||||
|
|
||||||
schema_as_json = json_loads(results["rows"][0]["DatabaseSchema"])
|
schema_as_json = json_loads(results["rows"][0]["DatabaseSchema"])
|
||||||
tables_list = schema_as_json["Databases"][self.configuration["database"]]["Tables"].values()
|
tables_list = [
|
||||||
|
*(schema_as_json["Databases"][self.configuration["database"]]["Tables"].values()),
|
||||||
|
*(schema_as_json["Databases"][self.configuration["database"]]["MaterializedViews"].values()),
|
||||||
|
]
|
||||||
|
|
||||||
schema = {}
|
schema = {}
|
||||||
|
|
||||||
@@ -154,7 +216,9 @@ class AzureKusto(BaseQueryRunner):
|
|||||||
schema[table_name] = {"name": table_name, "columns": []}
|
schema[table_name] = {"name": table_name, "columns": []}
|
||||||
|
|
||||||
for column in table["OrderedColumns"]:
|
for column in table["OrderedColumns"]:
|
||||||
schema[table_name]["columns"].append(column["Name"])
|
schema[table_name]["columns"].append(
|
||||||
|
{"name": column["Name"], "type": TYPES_MAP.get(column["CslType"], None)}
|
||||||
|
)
|
||||||
|
|
||||||
return list(schema.values())
|
return list(schema.values())
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from redash.query_runner import (
|
|||||||
TYPE_FLOAT,
|
TYPE_FLOAT,
|
||||||
TYPE_INTEGER,
|
TYPE_INTEGER,
|
||||||
TYPE_STRING,
|
TYPE_STRING,
|
||||||
BaseQueryRunner,
|
BaseSQLQueryRunner,
|
||||||
InterruptException,
|
InterruptException,
|
||||||
JobTimeoutException,
|
JobTimeoutException,
|
||||||
register,
|
register,
|
||||||
@@ -86,7 +86,7 @@ def _get_query_results(jobs, project_id, location, job_id, start_index):
|
|||||||
).execute()
|
).execute()
|
||||||
logging.debug("query_reply %s", query_reply)
|
logging.debug("query_reply %s", query_reply)
|
||||||
if not query_reply["jobComplete"]:
|
if not query_reply["jobComplete"]:
|
||||||
time.sleep(10)
|
time.sleep(1)
|
||||||
return _get_query_results(jobs, project_id, location, job_id, start_index)
|
return _get_query_results(jobs, project_id, location, job_id, start_index)
|
||||||
|
|
||||||
return query_reply
|
return query_reply
|
||||||
@@ -98,7 +98,7 @@ def _get_total_bytes_processed_for_resp(bq_response):
|
|||||||
return int(bq_response.get("totalBytesProcessed", "0"))
|
return int(bq_response.get("totalBytesProcessed", "0"))
|
||||||
|
|
||||||
|
|
||||||
class BigQuery(BaseQueryRunner):
|
class BigQuery(BaseSQLQueryRunner):
|
||||||
noop_query = "SELECT 1"
|
noop_query = "SELECT 1"
|
||||||
|
|
||||||
def __init__(self, configuration):
|
def __init__(self, configuration):
|
||||||
@@ -156,6 +156,11 @@ class BigQuery(BaseQueryRunner):
|
|||||||
"secret": ["jsonKeyFile"],
|
"secret": ["jsonKeyFile"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def annotate_query(self, query, metadata):
|
||||||
|
# Remove "Job ID" before annotating the query to avoid cache misses
|
||||||
|
metadata = {k: v for k, v in metadata.items() if k != "Job ID"}
|
||||||
|
return super().annotate_query(query, metadata)
|
||||||
|
|
||||||
def _get_bigquery_service(self):
|
def _get_bigquery_service(self):
|
||||||
socket.setdefaulttimeout(settings.BIGQUERY_HTTP_TIMEOUT)
|
socket.setdefaulttimeout(settings.BIGQUERY_HTTP_TIMEOUT)
|
||||||
|
|
||||||
@@ -215,11 +220,12 @@ class BigQuery(BaseQueryRunner):
|
|||||||
job_data = self._get_job_data(query)
|
job_data = self._get_job_data(query)
|
||||||
insert_response = jobs.insert(projectId=project_id, body=job_data).execute()
|
insert_response = jobs.insert(projectId=project_id, body=job_data).execute()
|
||||||
self.current_job_id = insert_response["jobReference"]["jobId"]
|
self.current_job_id = insert_response["jobReference"]["jobId"]
|
||||||
|
self.current_job_location = insert_response["jobReference"]["location"]
|
||||||
current_row = 0
|
current_row = 0
|
||||||
query_reply = _get_query_results(
|
query_reply = _get_query_results(
|
||||||
jobs,
|
jobs,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
location=self._get_location(),
|
location=self.current_job_location,
|
||||||
job_id=self.current_job_id,
|
job_id=self.current_job_id,
|
||||||
start_index=current_row,
|
start_index=current_row,
|
||||||
)
|
)
|
||||||
@@ -236,13 +242,11 @@ class BigQuery(BaseQueryRunner):
|
|||||||
|
|
||||||
query_result_request = {
|
query_result_request = {
|
||||||
"projectId": project_id,
|
"projectId": project_id,
|
||||||
"jobId": query_reply["jobReference"]["jobId"],
|
"jobId": self.current_job_id,
|
||||||
"startIndex": current_row,
|
"startIndex": current_row,
|
||||||
|
"location": self.current_job_location,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self._get_location():
|
|
||||||
query_result_request["location"] = self._get_location()
|
|
||||||
|
|
||||||
query_reply = jobs.getQueryResults(**query_result_request).execute()
|
query_reply = jobs.getQueryResults(**query_result_request).execute()
|
||||||
|
|
||||||
columns = [
|
columns = [
|
||||||
@@ -304,28 +308,70 @@ class BigQuery(BaseQueryRunner):
|
|||||||
datasets = self._get_project_datasets(project_id)
|
datasets = self._get_project_datasets(project_id)
|
||||||
|
|
||||||
query_base = """
|
query_base = """
|
||||||
SELECT table_schema, table_name, field_path, data_type
|
SELECT table_schema, table_name, field_path, data_type, description
|
||||||
FROM `{dataset_id}`.INFORMATION_SCHEMA.COLUMN_FIELD_PATHS
|
FROM `{dataset_id}`.INFORMATION_SCHEMA.COLUMN_FIELD_PATHS
|
||||||
WHERE table_schema NOT IN ('information_schema')
|
WHERE table_schema NOT IN ('information_schema')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
table_query_base = """
|
||||||
|
SELECT table_schema, table_name, JSON_VALUE(option_value) as table_description
|
||||||
|
FROM `{dataset_id}`.INFORMATION_SCHEMA.TABLE_OPTIONS
|
||||||
|
WHERE table_schema NOT IN ('information_schema')
|
||||||
|
AND option_name = 'description'
|
||||||
|
"""
|
||||||
|
|
||||||
|
location_dataset_ids = {}
|
||||||
schema = {}
|
schema = {}
|
||||||
queries = []
|
|
||||||
for dataset in datasets:
|
for dataset in datasets:
|
||||||
dataset_id = dataset["datasetReference"]["datasetId"]
|
dataset_id = dataset["datasetReference"]["datasetId"]
|
||||||
query = query_base.format(dataset_id=dataset_id)
|
location = dataset["location"]
|
||||||
queries.append(query)
|
if self._get_location() and location != self._get_location():
|
||||||
|
logger.debug("dataset location is different: %s", location)
|
||||||
|
continue
|
||||||
|
|
||||||
query = "\nUNION ALL\n".join(queries)
|
if location not in location_dataset_ids:
|
||||||
results, error = self.run_query(query, None)
|
location_dataset_ids[location] = []
|
||||||
if error is not None:
|
location_dataset_ids[location].append(dataset_id)
|
||||||
self._handle_run_query_error(error)
|
|
||||||
|
|
||||||
for row in results["rows"]:
|
for location, datasets in location_dataset_ids.items():
|
||||||
table_name = "{0}.{1}".format(row["table_schema"], row["table_name"])
|
queries = []
|
||||||
if table_name not in schema:
|
for dataset_id in datasets:
|
||||||
schema[table_name] = {"name": table_name, "columns": []}
|
query = query_base.format(dataset_id=dataset_id)
|
||||||
schema[table_name]["columns"].append({"name": row["field_path"], "type": row["data_type"]})
|
queries.append(query)
|
||||||
|
|
||||||
|
query = "\nUNION ALL\n".join(queries)
|
||||||
|
results, error = self.run_query(query, None)
|
||||||
|
if error is not None:
|
||||||
|
self._handle_run_query_error(error)
|
||||||
|
|
||||||
|
for row in results["rows"]:
|
||||||
|
table_name = "{0}.{1}".format(row["table_schema"], row["table_name"])
|
||||||
|
if table_name not in schema:
|
||||||
|
schema[table_name] = {"name": table_name, "columns": []}
|
||||||
|
schema[table_name]["columns"].append(
|
||||||
|
{
|
||||||
|
"name": row["field_path"],
|
||||||
|
"type": row["data_type"],
|
||||||
|
"description": row["description"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
table_queries = []
|
||||||
|
for dataset_id in datasets:
|
||||||
|
table_query = table_query_base.format(dataset_id=dataset_id)
|
||||||
|
table_queries.append(table_query)
|
||||||
|
|
||||||
|
table_query = "\nUNION ALL\n".join(table_queries)
|
||||||
|
results, error = self.run_query(table_query, None)
|
||||||
|
if error is not None:
|
||||||
|
self._handle_run_query_error(error)
|
||||||
|
|
||||||
|
for row in results["rows"]:
|
||||||
|
table_name = "{0}.{1}".format(row["table_schema"], row["table_name"])
|
||||||
|
if table_name not in schema:
|
||||||
|
schema[table_name] = {"name": table_name, "columns": []}
|
||||||
|
if "table_description" in row:
|
||||||
|
schema[table_name]["description"] = row["table_description"]
|
||||||
|
|
||||||
return list(schema.values())
|
return list(schema.values())
|
||||||
|
|
||||||
@@ -359,7 +405,7 @@ class BigQuery(BaseQueryRunner):
|
|||||||
self._get_bigquery_service().jobs().cancel(
|
self._get_bigquery_service().jobs().cancel(
|
||||||
projectId=self._get_project_id(),
|
projectId=self._get_project_id(),
|
||||||
jobId=self.current_job_id,
|
jobId=self.current_job_id,
|
||||||
location=self._get_location(),
|
location=self.current_job_location,
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -77,7 +77,11 @@ class ClickHouse(BaseSQLQueryRunner):
|
|||||||
self._url = self._url._replace(netloc="{}:{}".format(self._url.hostname, port))
|
self._url = self._url._replace(netloc="{}:{}".format(self._url.hostname, port))
|
||||||
|
|
||||||
def _get_tables(self, schema):
|
def _get_tables(self, schema):
|
||||||
query = "SELECT database, table, name FROM system.columns WHERE database NOT IN ('system')"
|
query = """
|
||||||
|
SELECT database, table, name, type as data_type
|
||||||
|
FROM system.columns
|
||||||
|
WHERE database NOT IN ('system', 'information_schema', 'INFORMATION_SCHEMA')
|
||||||
|
"""
|
||||||
|
|
||||||
results, error = self.run_query(query, None)
|
results, error = self.run_query(query, None)
|
||||||
|
|
||||||
@@ -90,7 +94,7 @@ class ClickHouse(BaseSQLQueryRunner):
|
|||||||
if table_name not in schema:
|
if table_name not in schema:
|
||||||
schema[table_name] = {"name": table_name, "columns": []}
|
schema[table_name] = {"name": table_name, "columns": []}
|
||||||
|
|
||||||
schema[table_name]["columns"].append(row["name"])
|
schema[table_name]["columns"].append({"name": row["name"], "type": row["data_type"]})
|
||||||
|
|
||||||
return list(schema.values())
|
return list(schema.values())
|
||||||
|
|
||||||
|
|||||||
@@ -34,9 +34,13 @@ class ResultSet:
|
|||||||
|
|
||||||
def parse_issue(issue, field_mapping): # noqa: C901
|
def parse_issue(issue, field_mapping): # noqa: C901
|
||||||
result = OrderedDict()
|
result = OrderedDict()
|
||||||
result["key"] = issue["key"]
|
|
||||||
|
|
||||||
for k, v in issue["fields"].items(): #
|
# Handle API v3 response format: key field may be missing, use id as fallback
|
||||||
|
result["key"] = issue.get("key", issue.get("id", "unknown"))
|
||||||
|
|
||||||
|
# Handle API v3 response format: fields may be missing
|
||||||
|
fields = issue.get("fields", {})
|
||||||
|
for k, v in fields.items(): #
|
||||||
output_name = field_mapping.get_output_field_name(k)
|
output_name = field_mapping.get_output_field_name(k)
|
||||||
member_names = field_mapping.get_dict_members(k)
|
member_names = field_mapping.get_dict_members(k)
|
||||||
|
|
||||||
@@ -98,7 +102,9 @@ def parse_issues(data, field_mapping):
|
|||||||
|
|
||||||
def parse_count(data):
|
def parse_count(data):
|
||||||
results = ResultSet()
|
results = ResultSet()
|
||||||
results.add_row({"count": data["total"]})
|
# API v3 may not return 'total' field, fallback to counting issues
|
||||||
|
count = data.get("total", len(data.get("issues", [])))
|
||||||
|
results.add_row({"count": count})
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
@@ -160,18 +166,26 @@ class JiraJQL(BaseHTTPQueryRunner):
|
|||||||
self.syntax = "json"
|
self.syntax = "json"
|
||||||
|
|
||||||
def run_query(self, query, user):
|
def run_query(self, query, user):
|
||||||
jql_url = "{}/rest/api/2/search".format(self.configuration["url"])
|
# Updated to API v3 endpoint, fix double slash issue
|
||||||
|
jql_url = "{}/rest/api/3/search/jql".format(self.configuration["url"].rstrip("/"))
|
||||||
|
|
||||||
query = json_loads(query)
|
query = json_loads(query)
|
||||||
query_type = query.pop("queryType", "select")
|
query_type = query.pop("queryType", "select")
|
||||||
field_mapping = FieldMapping(query.pop("fieldMapping", {}))
|
field_mapping = FieldMapping(query.pop("fieldMapping", {}))
|
||||||
|
|
||||||
|
# API v3 requires mandatory jql parameter with restrictions
|
||||||
|
if "jql" not in query or not query["jql"]:
|
||||||
|
query["jql"] = "created >= -30d order by created DESC"
|
||||||
|
|
||||||
if query_type == "count":
|
if query_type == "count":
|
||||||
query["maxResults"] = 1
|
query["maxResults"] = 1
|
||||||
query["fields"] = ""
|
query["fields"] = ""
|
||||||
else:
|
else:
|
||||||
query["maxResults"] = query.get("maxResults", 1000)
|
query["maxResults"] = query.get("maxResults", 1000)
|
||||||
|
|
||||||
|
if "fields" not in query:
|
||||||
|
query["fields"] = "*all"
|
||||||
|
|
||||||
response, error = self.get_response(jql_url, params=query)
|
response, error = self.get_response(jql_url, params=query)
|
||||||
if error is not None:
|
if error is not None:
|
||||||
return None, error
|
return None, error
|
||||||
@@ -182,17 +196,15 @@ class JiraJQL(BaseHTTPQueryRunner):
|
|||||||
results = parse_count(data)
|
results = parse_count(data)
|
||||||
else:
|
else:
|
||||||
results = parse_issues(data, field_mapping)
|
results = parse_issues(data, field_mapping)
|
||||||
index = data["startAt"] + data["maxResults"]
|
|
||||||
|
|
||||||
while data["total"] > index:
|
# API v3 uses token-based pagination instead of startAt/total
|
||||||
query["startAt"] = index
|
while not data.get("isLast", True) and "nextPageToken" in data:
|
||||||
|
query["nextPageToken"] = data["nextPageToken"]
|
||||||
response, error = self.get_response(jql_url, params=query)
|
response, error = self.get_response(jql_url, params=query)
|
||||||
if error is not None:
|
if error is not None:
|
||||||
return None, error
|
return None, error
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
index = data["startAt"] + data["maxResults"]
|
|
||||||
|
|
||||||
addl_results = parse_issues(data, field_mapping)
|
addl_results = parse_issues(data, field_mapping)
|
||||||
results.merge(addl_results)
|
results.merge(addl_results)
|
||||||
|
|
||||||
|
|||||||
@@ -215,10 +215,10 @@ class MongoDB(BaseQueryRunner):
|
|||||||
if readPreference:
|
if readPreference:
|
||||||
kwargs["readPreference"] = readPreference
|
kwargs["readPreference"] = readPreference
|
||||||
|
|
||||||
if "username" in self.configuration:
|
if self.configuration.get("username"):
|
||||||
kwargs["username"] = self.configuration["username"]
|
kwargs["username"] = self.configuration["username"]
|
||||||
|
|
||||||
if "password" in self.configuration:
|
if self.configuration.get("password"):
|
||||||
kwargs["password"] = self.configuration["password"]
|
kwargs["password"] = self.configuration["password"]
|
||||||
|
|
||||||
db_connection = pymongo.MongoClient(self.configuration["connectionString"], **kwargs)
|
db_connection = pymongo.MongoClient(self.configuration["connectionString"], **kwargs)
|
||||||
|
|||||||
@@ -150,7 +150,9 @@ class Mysql(BaseSQLQueryRunner):
|
|||||||
query = """
|
query = """
|
||||||
SELECT col.table_schema as table_schema,
|
SELECT col.table_schema as table_schema,
|
||||||
col.table_name as table_name,
|
col.table_name as table_name,
|
||||||
col.column_name as column_name
|
col.column_name as column_name,
|
||||||
|
col.data_type as data_type,
|
||||||
|
col.column_comment as column_comment
|
||||||
FROM `information_schema`.`columns` col
|
FROM `information_schema`.`columns` col
|
||||||
WHERE LOWER(col.table_schema) NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys');
|
WHERE LOWER(col.table_schema) NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys');
|
||||||
"""
|
"""
|
||||||
@@ -169,7 +171,38 @@ class Mysql(BaseSQLQueryRunner):
|
|||||||
if table_name not in schema:
|
if table_name not in schema:
|
||||||
schema[table_name] = {"name": table_name, "columns": []}
|
schema[table_name] = {"name": table_name, "columns": []}
|
||||||
|
|
||||||
schema[table_name]["columns"].append(row["column_name"])
|
schema[table_name]["columns"].append(
|
||||||
|
{
|
||||||
|
"name": row["column_name"],
|
||||||
|
"type": row["data_type"],
|
||||||
|
"description": row["column_comment"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
table_query = """
|
||||||
|
SELECT col.table_schema as table_schema,
|
||||||
|
col.table_name as table_name,
|
||||||
|
col.table_comment as table_comment
|
||||||
|
FROM `information_schema`.`tables` col
|
||||||
|
WHERE LOWER(col.table_schema) NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys'); \
|
||||||
|
"""
|
||||||
|
|
||||||
|
results, error = self.run_query(table_query, None)
|
||||||
|
|
||||||
|
if error is not None:
|
||||||
|
self._handle_run_query_error(error)
|
||||||
|
|
||||||
|
for row in results["rows"]:
|
||||||
|
if row["table_schema"] != self.configuration["db"]:
|
||||||
|
table_name = "{}.{}".format(row["table_schema"], row["table_name"])
|
||||||
|
else:
|
||||||
|
table_name = row["table_name"]
|
||||||
|
|
||||||
|
if table_name not in schema:
|
||||||
|
schema[table_name] = {"name": table_name, "columns": []}
|
||||||
|
|
||||||
|
if "table_comment" in row and row["table_comment"]:
|
||||||
|
schema[table_name]["description"] = row["table_comment"]
|
||||||
|
|
||||||
return list(schema.values())
|
return list(schema.values())
|
||||||
|
|
||||||
|
|||||||
@@ -205,24 +205,15 @@ class PostgreSQL(BaseSQLQueryRunner):
|
|||||||
|
|
||||||
def _get_tables(self, schema):
|
def _get_tables(self, schema):
|
||||||
"""
|
"""
|
||||||
relkind constants per https://www.postgresql.org/docs/10/static/catalog-pg-class.html
|
relkind constants from https://www.postgresql.org/docs/current/catalog-pg-class.html
|
||||||
r = regular table
|
|
||||||
v = view
|
|
||||||
m = materialized view
|
m = materialized view
|
||||||
f = foreign table
|
|
||||||
p = partitioned table (new in 10)
|
|
||||||
---
|
|
||||||
i = index
|
|
||||||
S = sequence
|
|
||||||
t = TOAST table
|
|
||||||
c = composite type
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
SELECT s.nspname as table_schema,
|
SELECT s.nspname AS table_schema,
|
||||||
c.relname as table_name,
|
c.relname AS table_name,
|
||||||
a.attname as column_name,
|
a.attname AS column_name,
|
||||||
null as data_type
|
NULL AS data_type
|
||||||
FROM pg_class c
|
FROM pg_class c
|
||||||
JOIN pg_namespace s
|
JOIN pg_namespace s
|
||||||
ON c.relnamespace = s.oid
|
ON c.relnamespace = s.oid
|
||||||
@@ -231,7 +222,7 @@ class PostgreSQL(BaseSQLQueryRunner):
|
|||||||
ON a.attrelid = c.oid
|
ON a.attrelid = c.oid
|
||||||
AND a.attnum > 0
|
AND a.attnum > 0
|
||||||
AND NOT a.attisdropped
|
AND NOT a.attisdropped
|
||||||
WHERE c.relkind IN ('m', 'f', 'p')
|
WHERE c.relkind = 'm'
|
||||||
AND has_table_privilege(s.nspname || '.' || c.relname, 'select')
|
AND has_table_privilege(s.nspname || '.' || c.relname, 'select')
|
||||||
AND has_schema_privilege(s.nspname, 'usage')
|
AND has_schema_privilege(s.nspname, 'usage')
|
||||||
|
|
||||||
@@ -243,6 +234,8 @@ class PostgreSQL(BaseSQLQueryRunner):
|
|||||||
data_type
|
data_type
|
||||||
FROM information_schema.columns
|
FROM information_schema.columns
|
||||||
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
||||||
|
AND has_table_privilege(table_schema || '.' || table_name, 'select')
|
||||||
|
AND has_schema_privilege(table_schema, 'usage')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self._get_definitions(schema, query)
|
self._get_definitions(schema, query)
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
try:
|
try:
|
||||||
import snowflake.connector
|
import snowflake.connector
|
||||||
|
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||||
|
|
||||||
enabled = True
|
enabled = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
enabled = False
|
enabled = False
|
||||||
|
|
||||||
|
|
||||||
|
from base64 import b64decode
|
||||||
|
|
||||||
from redash import __version__
|
from redash import __version__
|
||||||
from redash.query_runner import (
|
from redash.query_runner import (
|
||||||
TYPE_BOOLEAN,
|
TYPE_BOOLEAN,
|
||||||
@@ -43,6 +46,8 @@ class Snowflake(BaseSQLQueryRunner):
|
|||||||
"account": {"type": "string"},
|
"account": {"type": "string"},
|
||||||
"user": {"type": "string"},
|
"user": {"type": "string"},
|
||||||
"password": {"type": "string"},
|
"password": {"type": "string"},
|
||||||
|
"private_key_File": {"type": "string"},
|
||||||
|
"private_key_pwd": {"type": "string"},
|
||||||
"warehouse": {"type": "string"},
|
"warehouse": {"type": "string"},
|
||||||
"database": {"type": "string"},
|
"database": {"type": "string"},
|
||||||
"region": {"type": "string", "default": "us-west"},
|
"region": {"type": "string", "default": "us-west"},
|
||||||
@@ -57,13 +62,15 @@ class Snowflake(BaseSQLQueryRunner):
|
|||||||
"account",
|
"account",
|
||||||
"user",
|
"user",
|
||||||
"password",
|
"password",
|
||||||
|
"private_key_File",
|
||||||
|
"private_key_pwd",
|
||||||
"warehouse",
|
"warehouse",
|
||||||
"database",
|
"database",
|
||||||
"region",
|
"region",
|
||||||
"host",
|
"host",
|
||||||
],
|
],
|
||||||
"required": ["user", "password", "account", "database", "warehouse"],
|
"required": ["user", "account", "database", "warehouse"],
|
||||||
"secret": ["password"],
|
"secret": ["password", "private_key_File", "private_key_pwd"],
|
||||||
"extra_options": [
|
"extra_options": [
|
||||||
"host",
|
"host",
|
||||||
],
|
],
|
||||||
@@ -88,7 +95,7 @@ class Snowflake(BaseSQLQueryRunner):
|
|||||||
if region == "us-west":
|
if region == "us-west":
|
||||||
region = None
|
region = None
|
||||||
|
|
||||||
if self.configuration.__contains__("host"):
|
if self.configuration.get("host"):
|
||||||
host = self.configuration.get("host")
|
host = self.configuration.get("host")
|
||||||
else:
|
else:
|
||||||
if region:
|
if region:
|
||||||
@@ -96,14 +103,29 @@ class Snowflake(BaseSQLQueryRunner):
|
|||||||
else:
|
else:
|
||||||
host = "{}.snowflakecomputing.com".format(account)
|
host = "{}.snowflakecomputing.com".format(account)
|
||||||
|
|
||||||
connection = snowflake.connector.connect(
|
params = {
|
||||||
user=self.configuration["user"],
|
"user": self.configuration["user"],
|
||||||
password=self.configuration["password"],
|
"account": account,
|
||||||
account=account,
|
"region": region,
|
||||||
region=region,
|
"host": host,
|
||||||
host=host,
|
"application": "Redash/{} (Snowflake)".format(__version__.split("-")[0]),
|
||||||
application="Redash/{} (Snowflake)".format(__version__.split("-")[0]),
|
}
|
||||||
)
|
|
||||||
|
if self.configuration.get("password"):
|
||||||
|
params["password"] = self.configuration["password"]
|
||||||
|
elif self.configuration.get("private_key_File"):
|
||||||
|
private_key_b64 = self.configuration.get("private_key_File")
|
||||||
|
private_key_bytes = b64decode(private_key_b64)
|
||||||
|
if self.configuration.get("private_key_pwd"):
|
||||||
|
private_key_pwd = self.configuration.get("private_key_pwd").encode()
|
||||||
|
else:
|
||||||
|
private_key_pwd = None
|
||||||
|
private_key_pem = load_pem_private_key(private_key_bytes, private_key_pwd)
|
||||||
|
params["private_key"] = private_key_pem
|
||||||
|
else:
|
||||||
|
raise Exception("Neither password nor private_key_b64 is set.")
|
||||||
|
|
||||||
|
connection = snowflake.connector.connect(**params)
|
||||||
|
|
||||||
return connection
|
return connection
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import functools
|
import functools
|
||||||
|
|
||||||
from flask import session
|
from flask import request, session
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_talisman import talisman
|
from flask_talisman import talisman
|
||||||
from flask_wtf.csrf import CSRFProtect, generate_csrf
|
from flask_wtf.csrf import CSRFProtect, generate_csrf
|
||||||
@@ -25,6 +25,7 @@ def init_app(app):
|
|||||||
app.config["WTF_CSRF_CHECK_DEFAULT"] = False
|
app.config["WTF_CSRF_CHECK_DEFAULT"] = False
|
||||||
app.config["WTF_CSRF_SSL_STRICT"] = False
|
app.config["WTF_CSRF_SSL_STRICT"] = False
|
||||||
app.config["WTF_CSRF_TIME_LIMIT"] = settings.CSRF_TIME_LIMIT
|
app.config["WTF_CSRF_TIME_LIMIT"] = settings.CSRF_TIME_LIMIT
|
||||||
|
app.config["SESSION_COOKIE_NAME"] = settings.SESSION_COOKIE_NAME
|
||||||
|
|
||||||
@app.after_request
|
@app.after_request
|
||||||
def inject_csrf_token(response):
|
def inject_csrf_token(response):
|
||||||
@@ -35,6 +36,15 @@ def init_app(app):
|
|||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def check_csrf():
|
def check_csrf():
|
||||||
|
# BEGIN workaround until https://github.com/lepture/flask-wtf/pull/419 is merged
|
||||||
|
if request.blueprint in csrf._exempt_blueprints:
|
||||||
|
return
|
||||||
|
|
||||||
|
view = app.view_functions.get(request.endpoint)
|
||||||
|
if view is not None and f"{view.__module__}.{view.__name__}" in csrf._exempt_views:
|
||||||
|
return
|
||||||
|
# END workaround
|
||||||
|
|
||||||
if not current_user.is_authenticated or "user_id" in session:
|
if not current_user.is_authenticated or "user_id" in session:
|
||||||
csrf.protect()
|
csrf.protect()
|
||||||
|
|
||||||
|
|||||||
@@ -82,9 +82,19 @@ class QuerySerializer(Serializer):
|
|||||||
else:
|
else:
|
||||||
result = [serialize_query(query, **self.options) for query in self.object_or_list]
|
result = [serialize_query(query, **self.options) for query in self.object_or_list]
|
||||||
if self.options.get("with_favorite_state", True):
|
if self.options.get("with_favorite_state", True):
|
||||||
favorite_ids = models.Favorite.are_favorites(current_user.id, self.object_or_list)
|
queries = list(self.object_or_list)
|
||||||
|
favorites = models.Favorite.query.filter(
|
||||||
|
models.Favorite.object_id.in_([o.id for o in queries]),
|
||||||
|
models.Favorite.object_type == "Query",
|
||||||
|
models.Favorite.user_id == current_user.id,
|
||||||
|
)
|
||||||
|
favorites_dict = {fav.object_id: fav for fav in favorites}
|
||||||
|
|
||||||
for query in result:
|
for query in result:
|
||||||
query["is_favorite"] = query["id"] in favorite_ids
|
favorite = favorites_dict.get(query["id"])
|
||||||
|
query["is_favorite"] = favorite is not None
|
||||||
|
if favorite:
|
||||||
|
query["starred_at"] = favorite.created_at
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -263,9 +273,19 @@ class DashboardSerializer(Serializer):
|
|||||||
else:
|
else:
|
||||||
result = [serialize_dashboard(obj, **self.options) for obj in self.object_or_list]
|
result = [serialize_dashboard(obj, **self.options) for obj in self.object_or_list]
|
||||||
if self.options.get("with_favorite_state", True):
|
if self.options.get("with_favorite_state", True):
|
||||||
favorite_ids = models.Favorite.are_favorites(current_user.id, self.object_or_list)
|
dashboards = list(self.object_or_list)
|
||||||
for obj in result:
|
favorites = models.Favorite.query.filter(
|
||||||
obj["is_favorite"] = obj["id"] in favorite_ids
|
models.Favorite.object_id.in_([o.id for o in dashboards]),
|
||||||
|
models.Favorite.object_type == "Dashboard",
|
||||||
|
models.Favorite.user_id == current_user.id,
|
||||||
|
)
|
||||||
|
favorites_dict = {fav.object_id: fav for fav in favorites}
|
||||||
|
|
||||||
|
for query in result:
|
||||||
|
favorite = favorites_dict.get(query["id"])
|
||||||
|
query["is_favorite"] = favorite is not None
|
||||||
|
if favorite:
|
||||||
|
query["starred_at"] = favorite.created_at
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ SESSION_COOKIE_SECURE = parse_boolean(os.environ.get("REDASH_SESSION_COOKIE_SECU
|
|||||||
# Whether the session cookie is set HttpOnly.
|
# Whether the session cookie is set HttpOnly.
|
||||||
SESSION_COOKIE_HTTPONLY = parse_boolean(os.environ.get("REDASH_SESSION_COOKIE_HTTPONLY", "true"))
|
SESSION_COOKIE_HTTPONLY = parse_boolean(os.environ.get("REDASH_SESSION_COOKIE_HTTPONLY", "true"))
|
||||||
SESSION_EXPIRY_TIME = int(os.environ.get("REDASH_SESSION_EXPIRY_TIME", 60 * 60 * 6))
|
SESSION_EXPIRY_TIME = int(os.environ.get("REDASH_SESSION_EXPIRY_TIME", 60 * 60 * 6))
|
||||||
|
SESSION_COOKIE_NAME = os.environ.get("REDASH_SESSION_COOKIE_NAME", "session")
|
||||||
|
|
||||||
# Whether the session cookie is set to secure.
|
# Whether the session cookie is set to secure.
|
||||||
REMEMBER_COOKIE_SECURE = parse_boolean(os.environ.get("REDASH_REMEMBER_COOKIE_SECURE") or str(COOKIES_SECURE))
|
REMEMBER_COOKIE_SECURE = parse_boolean(os.environ.get("REDASH_REMEMBER_COOKIE_SECURE") or str(COOKIES_SECURE))
|
||||||
@@ -135,6 +136,13 @@ FEATURE_POLICY = os.environ.get("REDASH_FEATURE_POLICY", "")
|
|||||||
|
|
||||||
MULTI_ORG = parse_boolean(os.environ.get("REDASH_MULTI_ORG", "false"))
|
MULTI_ORG = parse_boolean(os.environ.get("REDASH_MULTI_ORG", "false"))
|
||||||
|
|
||||||
|
# If Redash is behind a proxy it might sometimes receive a X-Forwarded-Proto of HTTP
|
||||||
|
# even if your actual Redash URL scheme is HTTPS. This will cause Flask to build
|
||||||
|
# the OAuth redirect URL incorrectly thus failing auth. This is especially common if
|
||||||
|
# you're behind a SSL/TCP configured AWS ELB or similar.
|
||||||
|
# This setting will force the URL scheme.
|
||||||
|
GOOGLE_OAUTH_SCHEME_OVERRIDE = os.environ.get("REDASH_GOOGLE_OAUTH_SCHEME_OVERRIDE", "")
|
||||||
|
|
||||||
GOOGLE_CLIENT_ID = os.environ.get("REDASH_GOOGLE_CLIENT_ID", "")
|
GOOGLE_CLIENT_ID = os.environ.get("REDASH_GOOGLE_CLIENT_ID", "")
|
||||||
GOOGLE_CLIENT_SECRET = os.environ.get("REDASH_GOOGLE_CLIENT_SECRET", "")
|
GOOGLE_CLIENT_SECRET = os.environ.get("REDASH_GOOGLE_CLIENT_SECRET", "")
|
||||||
GOOGLE_OAUTH_ENABLED = bool(GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET)
|
GOOGLE_OAUTH_ENABLED = bool(GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ DATE_FORMAT = os.environ.get("REDASH_DATE_FORMAT", "DD/MM/YY")
|
|||||||
TIME_FORMAT = os.environ.get("REDASH_TIME_FORMAT", "HH:mm")
|
TIME_FORMAT = os.environ.get("REDASH_TIME_FORMAT", "HH:mm")
|
||||||
INTEGER_FORMAT = os.environ.get("REDASH_INTEGER_FORMAT", "0,0")
|
INTEGER_FORMAT = os.environ.get("REDASH_INTEGER_FORMAT", "0,0")
|
||||||
FLOAT_FORMAT = os.environ.get("REDASH_FLOAT_FORMAT", "0,0.00")
|
FLOAT_FORMAT = os.environ.get("REDASH_FLOAT_FORMAT", "0,0.00")
|
||||||
|
NULL_VALUE = os.environ.get("REDASH_NULL_VALUE", "null")
|
||||||
MULTI_BYTE_SEARCH_ENABLED = parse_boolean(os.environ.get("MULTI_BYTE_SEARCH_ENABLED", "false"))
|
MULTI_BYTE_SEARCH_ENABLED = parse_boolean(os.environ.get("MULTI_BYTE_SEARCH_ENABLED", "false"))
|
||||||
|
|
||||||
JWT_LOGIN_ENABLED = parse_boolean(os.environ.get("REDASH_JWT_LOGIN_ENABLED", "false"))
|
JWT_LOGIN_ENABLED = parse_boolean(os.environ.get("REDASH_JWT_LOGIN_ENABLED", "false"))
|
||||||
@@ -59,6 +60,7 @@ settings = {
|
|||||||
"time_format": TIME_FORMAT,
|
"time_format": TIME_FORMAT,
|
||||||
"integer_format": INTEGER_FORMAT,
|
"integer_format": INTEGER_FORMAT,
|
||||||
"float_format": FLOAT_FORMAT,
|
"float_format": FLOAT_FORMAT,
|
||||||
|
"null_value": NULL_VALUE,
|
||||||
"multi_byte_search_enabled": MULTI_BYTE_SEARCH_ENABLED,
|
"multi_byte_search_enabled": MULTI_BYTE_SEARCH_ENABLED,
|
||||||
"auth_jwt_login_enabled": JWT_LOGIN_ENABLED,
|
"auth_jwt_login_enabled": JWT_LOGIN_ENABLED,
|
||||||
"auth_jwt_auth_issuer": JWT_AUTH_ISSUER,
|
"auth_jwt_auth_issuer": JWT_AUTH_ISSUER,
|
||||||
|
|||||||
42
tests/query_runner/test_azure_kusto.py
Normal file
42
tests/query_runner/test_azure_kusto.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from redash.query_runner.azure_kusto import AzureKusto
|
||||||
|
|
||||||
|
|
||||||
|
class TestAzureKusto(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.configuration = {
|
||||||
|
"cluster": "https://example.kusto.windows.net",
|
||||||
|
"database": "sample_db",
|
||||||
|
"azure_ad_client_id": "client_id",
|
||||||
|
"azure_ad_client_secret": "client_secret",
|
||||||
|
"azure_ad_tenant_id": "tenant_id",
|
||||||
|
}
|
||||||
|
self.kusto = AzureKusto(self.configuration)
|
||||||
|
|
||||||
|
@patch.object(AzureKusto, "run_query")
|
||||||
|
def test_get_schema(self, mock_run_query):
|
||||||
|
mock_response = {
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"DatabaseSchema": '{"Databases":{"sample_db":{"Tables":{"Table1":{"Name":"Table1","OrderedColumns":[{"Name":"Column1","Type":"System.String","CslType":"string"},{"Name":"Column2","Type":"System.DateTime","CslType":"datetime"}]}},"MaterializedViews":{"View1":{"Name":"View1","OrderedColumns":[{"Name":"Column1","Type":"System.String","CslType":"string"},{"Name":"Column2","Type":"System.DateTime","CslType":"datetime"}]}}}}}'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
mock_run_query.return_value = (mock_response, None)
|
||||||
|
|
||||||
|
expected_schema = [
|
||||||
|
{
|
||||||
|
"name": "Table1",
|
||||||
|
"columns": [{"name": "Column1", "type": "string"}, {"name": "Column2", "type": "datetime"}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "View1",
|
||||||
|
"columns": [{"name": "Column1", "type": "string"}, {"name": "Column2", "type": "datetime"}],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
schema = self.kusto.get_schema()
|
||||||
|
print(schema)
|
||||||
|
self.assertEqual(schema, expected_schema)
|
||||||
@@ -20,7 +20,7 @@ class TestBigQueryQueryRunner(unittest.TestCase):
|
|||||||
query = "SELECT a FROM tbl"
|
query = "SELECT a FROM tbl"
|
||||||
expect = (
|
expect = (
|
||||||
"/* Username: username, query_id: adhoc, "
|
"/* Username: username, query_id: adhoc, "
|
||||||
"Job ID: job-id, Query Hash: query-hash, "
|
"Query Hash: query-hash, "
|
||||||
"Scheduled: False */ SELECT a FROM tbl"
|
"Scheduled: False */ SELECT a FROM tbl"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -28,4 +28,4 @@ class TestJsonDumps(BaseTestCase):
|
|||||||
}
|
}
|
||||||
json_data = json_dumps(input_data)
|
json_data = json_dumps(input_data)
|
||||||
actual_output_data = json_loads(json_data)
|
actual_output_data = json_loads(json_data)
|
||||||
self.assertEquals(actual_output_data, expected_output_data)
|
self.assertEqual(actual_output_data, expected_output_data)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
"@types/jest": "^26.0.18",
|
"@types/jest": "^26.0.18",
|
||||||
"@types/leaflet": "^1.5.19",
|
"@types/leaflet": "^1.5.19",
|
||||||
"@types/numeral": "0.0.28",
|
"@types/numeral": "0.0.28",
|
||||||
"@types/plotly.js": "^2.35.2",
|
"@types/plotly.js": "^3.0.3",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.0",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.0",
|
||||||
"@types/tinycolor2": "^1.4.2",
|
"@types/tinycolor2": "^1.4.2",
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
"less-loader": "^11.1.3",
|
"less-loader": "^11.1.3",
|
||||||
"less-plugin-autoprefix": "^2.0.0",
|
"less-plugin-autoprefix": "^2.0.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "3.3.2",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"style-loader": "^3.3.3",
|
"style-loader": "^3.3.3",
|
||||||
"ts-migrate": "^0.1.35",
|
"ts-migrate": "^0.1.35",
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
"leaflet.markercluster": "^1.1.0",
|
"leaflet.markercluster": "^1.1.0",
|
||||||
"lodash": "^4.17.10",
|
"lodash": "^4.17.10",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"plotly.js": "2.35.3",
|
"plotly.js": "3.1.0",
|
||||||
"react-pivottable": "^0.9.0",
|
"react-pivottable": "^0.9.0",
|
||||||
"react-sortable-hoc": "^1.10.1",
|
"react-sortable-hoc": "^1.10.1",
|
||||||
"tinycolor2": "^1.4.1",
|
"tinycolor2": "^1.4.1",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import numeral from "numeral";
|
|||||||
import { isString, isArray, isUndefined, isFinite, isNil, toString } from "lodash";
|
import { isString, isArray, isUndefined, isFinite, isNil, toString } from "lodash";
|
||||||
import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
|
import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
|
||||||
|
|
||||||
|
|
||||||
numeral.options.scalePercentBy100 = false;
|
numeral.options.scalePercentBy100 = false;
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
@@ -12,9 +13,16 @@ const urlPattern = /(^|[\s\n]|<br\/?>)((?:https?|ftp):\/\/[\-A-Z0-9+\u0026\u2019
|
|||||||
|
|
||||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||||
|
|
||||||
|
function NullValueComponent() {
|
||||||
|
return <span className="display-as-null">{visualizationsSettings.nullValue}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
export function createTextFormatter(highlightLinks: any) {
|
export function createTextFormatter(highlightLinks: any) {
|
||||||
if (highlightLinks) {
|
if (highlightLinks) {
|
||||||
return (value: any) => {
|
return (value: any) => {
|
||||||
|
if (value === null) {
|
||||||
|
return <NullValueComponent/>
|
||||||
|
}
|
||||||
if (isString(value)) {
|
if (isString(value)) {
|
||||||
const Link = visualizationsSettings.LinkComponent;
|
const Link = visualizationsSettings.LinkComponent;
|
||||||
value = value.replace(urlPattern, (unused, prefix, href) => {
|
value = value.replace(urlPattern, (unused, prefix, href) => {
|
||||||
@@ -29,7 +37,7 @@ export function createTextFormatter(highlightLinks: any) {
|
|||||||
return toString(value);
|
return toString(value);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return (value: any) => toString(value);
|
return (value: any) => value === null ? <NullValueComponent/> : toString(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toMoment(value: any) {
|
function toMoment(value: any) {
|
||||||
@@ -46,11 +54,14 @@ function toMoment(value: any) {
|
|||||||
export function createDateTimeFormatter(format: any) {
|
export function createDateTimeFormatter(format: any) {
|
||||||
if (isString(format) && format !== "") {
|
if (isString(format) && format !== "") {
|
||||||
return (value: any) => {
|
return (value: any) => {
|
||||||
|
if (value === null) {
|
||||||
|
return <NullValueComponent/>;
|
||||||
|
}
|
||||||
const wrapped = toMoment(value);
|
const wrapped = toMoment(value);
|
||||||
return wrapped.isValid() ? wrapped.format(format) : toString(value);
|
return wrapped.isValid() ? wrapped.format(format) : toString(value);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return (value: any) => toString(value);
|
return (value: any) => value === null ? <NullValueComponent/> : toString(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBooleanFormatter(values: any) {
|
export function createBooleanFormatter(values: any) {
|
||||||
@@ -58,6 +69,9 @@ export function createBooleanFormatter(values: any) {
|
|||||||
if (values.length >= 2) {
|
if (values.length >= 2) {
|
||||||
// Both `true` and `false` specified
|
// Both `true` and `false` specified
|
||||||
return (value: any) => {
|
return (value: any) => {
|
||||||
|
if (value === null) {
|
||||||
|
return <NullValueComponent/>;
|
||||||
|
}
|
||||||
if (isNil(value)) {
|
if (isNil(value)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -69,6 +83,9 @@ export function createBooleanFormatter(values: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (value: any) => {
|
return (value: any) => {
|
||||||
|
if (value === null) {
|
||||||
|
return <NullValueComponent/>;
|
||||||
|
}
|
||||||
if (isNil(value)) {
|
if (isNil(value)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -76,12 +93,20 @@ export function createBooleanFormatter(values: any) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createNumberFormatter(format: any) {
|
export function createNumberFormatter(format: any, canReturnHTMLElement: boolean = false) {
|
||||||
if (isString(format) && format !== "") {
|
if (isString(format) && format !== "") {
|
||||||
const n = numeral(0); // cache `numeral` instance
|
const n = numeral(0); // cache `numeral` instance
|
||||||
return (value: any) => (value === null || value === "" ? "" : n.set(value).format(format));
|
return (value: any) => {
|
||||||
|
if (canReturnHTMLElement && value === null) {
|
||||||
|
return <NullValueComponent/>;
|
||||||
|
}
|
||||||
|
if (value === "" || value === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return n.set(value).format(format);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (value: any) => toString(value);
|
return (value: any) => (canReturnHTMLElement && value === null) ? <NullValueComponent/> : toString(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatSimpleTemplate(str: any, data: any) {
|
export function formatSimpleTemplate(str: any, data: any) {
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ export default function GeneralSettings({ options, data, onOptionsChange }: any)
|
|||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!includes(["custom", "heatmap", "bubble", "scatter"], options.globalSeriesType) && (
|
{!includes(["custom", "heatmap", "bubble"], options.globalSeriesType) && (
|
||||||
// @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message
|
// @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message
|
||||||
<Section>
|
<Section>
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default {
|
|||||||
Renderer,
|
Renderer,
|
||||||
Editor,
|
Editor,
|
||||||
|
|
||||||
defaultColumns: 3,
|
defaultColumns: 6,
|
||||||
defaultRows: 8,
|
defaultRows: 8,
|
||||||
minColumns: 1,
|
minColumns: 1,
|
||||||
minRows: 5,
|
minRows: 5,
|
||||||
|
|||||||
@@ -48,7 +48,6 @@
|
|||||||
"series": [
|
"series": [
|
||||||
{
|
{
|
||||||
"visible": true,
|
"visible": true,
|
||||||
"offsetgroup": "0",
|
|
||||||
"type": "bar",
|
"type": "bar",
|
||||||
"name": "a",
|
"name": "a",
|
||||||
"x": ["x1", "x2", "x3", "x4"],
|
"x": ["x1", "x2", "x3", "x4"],
|
||||||
@@ -64,7 +63,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"visible": true,
|
"visible": true,
|
||||||
"offsetgroup": "1",
|
|
||||||
"type": "bar",
|
"type": "bar",
|
||||||
"name": "b",
|
"name": "b",
|
||||||
"x": ["x1", "x2", "x3", "x4"],
|
"x": ["x1", "x2", "x3", "x4"],
|
||||||
|
|||||||
@@ -48,7 +48,6 @@
|
|||||||
"series": [
|
"series": [
|
||||||
{
|
{
|
||||||
"visible": true,
|
"visible": true,
|
||||||
"offsetgroup": "0",
|
|
||||||
"type": "bar",
|
"type": "bar",
|
||||||
"name": "a",
|
"name": "a",
|
||||||
"x": ["x1", "x2", "x3", "x4"],
|
"x": ["x1", "x2", "x3", "x4"],
|
||||||
@@ -64,7 +63,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"visible": true,
|
"visible": true,
|
||||||
"offsetgroup": "1",
|
|
||||||
"type": "bar",
|
"type": "bar",
|
||||||
"name": "b",
|
"name": "b",
|
||||||
"x": ["x1", "x2", "x3", "x4"],
|
"x": ["x1", "x2", "x3", "x4"],
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as Plotly from "plotly.js";
|
import * as Plotly from "plotly.js";
|
||||||
|
|
||||||
|
import "./locales"
|
||||||
import prepareData from "./prepareData";
|
import prepareData from "./prepareData";
|
||||||
import prepareLayout from "./prepareLayout";
|
import prepareLayout from "./prepareLayout";
|
||||||
import updateData from "./updateData";
|
import updateData from "./updateData";
|
||||||
@@ -7,10 +8,31 @@ import updateAxes from "./updateAxes";
|
|||||||
import updateChartSize from "./updateChartSize";
|
import updateChartSize from "./updateChartSize";
|
||||||
import { prepareCustomChartData, createCustomChartRenderer } from "./customChartUtils";
|
import { prepareCustomChartData, createCustomChartRenderer } from "./customChartUtils";
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'setPlotConfig' does not exist on type 't... Remove this comment to see the full error message
|
const rangeSliderIcon = {
|
||||||
|
'width': 400,
|
||||||
|
'height': 400,
|
||||||
|
'path': 'M50 180h300a20 20 0 0 1 0 40H50a20 20 0 0 1 0-40z M160 200a40 40 0 1 0 -80 0a40 40 0 1 0 80 0 M320 200a40 40 0 1 0 -80 0a40 40 0 1 0 80 0',
|
||||||
|
};
|
||||||
|
|
||||||
Plotly.setPlotConfig({
|
Plotly.setPlotConfig({
|
||||||
modeBarButtonsToRemove: ["sendDataToCloud"],
|
modeBarButtonsToRemove: ["sendDataToCloud"],
|
||||||
modeBarButtonsToAdd: ["togglespikelines", "v1hovermode"],
|
modeBarButtonsToAdd: ["togglespikelines", "v1hovermode",
|
||||||
|
{
|
||||||
|
name: 'toggleRangeslider',
|
||||||
|
title: 'Toggle rangeslider',
|
||||||
|
icon: rangeSliderIcon,
|
||||||
|
click: function(gd: any) {
|
||||||
|
if(gd?.layout?.xaxis) {
|
||||||
|
let newRangeslider: any = {};
|
||||||
|
if (gd.layout.xaxis?.rangeslider) {
|
||||||
|
newRangeslider = null;
|
||||||
|
}
|
||||||
|
(Plotly.relayout as any)(gd, 'xaxis.rangeslider', newRangeslider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
locale: window.navigator.language,
|
||||||
});
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
230
viz-lib/src/visualizations/chart/plotly/locales.ts
Normal file
230
viz-lib/src/visualizations/chart/plotly/locales.ts
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
import * as Plotly from "plotly.js";
|
||||||
|
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeAf from "plotly.js/lib/locales/af";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeAm from "plotly.js/lib/locales/am";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeAr_dz from "plotly.js/lib/locales/ar-dz";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeAr_eg from "plotly.js/lib/locales/ar-eg";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeAr from "plotly.js/lib/locales/ar";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeAz from "plotly.js/lib/locales/az";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeBg from "plotly.js/lib/locales/bg";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeBs from "plotly.js/lib/locales/bs";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeCa from "plotly.js/lib/locales/ca";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeCs from "plotly.js/lib/locales/cs";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeCy from "plotly.js/lib/locales/cy";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeDa from "plotly.js/lib/locales/da";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeDe_ch from "plotly.js/lib/locales/de-ch";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeDe from "plotly.js/lib/locales/de";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeEl from "plotly.js/lib/locales/el";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeEo from "plotly.js/lib/locales/eo";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeEs_ar from "plotly.js/lib/locales/es-ar";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeEs_pe from "plotly.js/lib/locales/es-pe";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeEs from "plotly.js/lib/locales/es";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeEt from "plotly.js/lib/locales/et";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeEu from "plotly.js/lib/locales/eu";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeFa from "plotly.js/lib/locales/fa";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeFi from "plotly.js/lib/locales/fi";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeFo from "plotly.js/lib/locales/fo";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeFr_ch from "plotly.js/lib/locales/fr-ch";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeFr from "plotly.js/lib/locales/fr";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeGl from "plotly.js/lib/locales/gl";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeGu from "plotly.js/lib/locales/gu";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeHe from "plotly.js/lib/locales/he";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeHi_in from "plotly.js/lib/locales/hi-in";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeHr from "plotly.js/lib/locales/hr";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeHu from "plotly.js/lib/locales/hu";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeHy from "plotly.js/lib/locales/hy";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeId from "plotly.js/lib/locales/id";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeIs from "plotly.js/lib/locales/is";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeIt from "plotly.js/lib/locales/it";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeJa from "plotly.js/lib/locales/ja";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeKa from "plotly.js/lib/locales/ka";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeKm from "plotly.js/lib/locales/km";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeKo from "plotly.js/lib/locales/ko";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeLt from "plotly.js/lib/locales/lt";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeLv from "plotly.js/lib/locales/lv";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeMe_me from "plotly.js/lib/locales/me-me";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeMe from "plotly.js/lib/locales/me";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeMk from "plotly.js/lib/locales/mk";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeMl from "plotly.js/lib/locales/ml";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeMs from "plotly.js/lib/locales/ms";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeMt from "plotly.js/lib/locales/mt";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeNl_be from "plotly.js/lib/locales/nl-be";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeNl from "plotly.js/lib/locales/nl";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeNo from "plotly.js/lib/locales/no";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localePa from "plotly.js/lib/locales/pa";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localePl from "plotly.js/lib/locales/pl";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localePt_br from "plotly.js/lib/locales/pt-br";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localePt_pt from "plotly.js/lib/locales/pt-pt";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeRm from "plotly.js/lib/locales/rm";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeRo from "plotly.js/lib/locales/ro";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeRu from "plotly.js/lib/locales/ru";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeSk from "plotly.js/lib/locales/sk";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeSl from "plotly.js/lib/locales/sl";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeSq from "plotly.js/lib/locales/sq";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeSr_sr from "plotly.js/lib/locales/sr-sr";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeSr from "plotly.js/lib/locales/sr";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeSv from "plotly.js/lib/locales/sv";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeSw from "plotly.js/lib/locales/sw";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeTa from "plotly.js/lib/locales/ta";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeTh from "plotly.js/lib/locales/th";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeTr from "plotly.js/lib/locales/tr";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeTt from "plotly.js/lib/locales/tt";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeUk from "plotly.js/lib/locales/uk";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeUr from "plotly.js/lib/locales/ur";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeVi from "plotly.js/lib/locales/vi";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeZh_cn from "plotly.js/lib/locales/zh-cn";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeZh_hk from "plotly.js/lib/locales/zh-hk";
|
||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||||
|
import localeZh_tw from "plotly.js/lib/locales/zh-tw";
|
||||||
|
|
||||||
|
(Plotly as any).register([
|
||||||
|
localeAf,
|
||||||
|
localeAm,
|
||||||
|
localeAr_dz,
|
||||||
|
localeAr_eg,
|
||||||
|
localeAr,
|
||||||
|
localeAz,
|
||||||
|
localeBg,
|
||||||
|
localeBs,
|
||||||
|
localeCa,
|
||||||
|
localeCs,
|
||||||
|
localeCy,
|
||||||
|
localeDa,
|
||||||
|
localeDe_ch,
|
||||||
|
localeDe,
|
||||||
|
localeEl,
|
||||||
|
localeEo,
|
||||||
|
localeEs_ar,
|
||||||
|
localeEs_pe,
|
||||||
|
localeEs,
|
||||||
|
localeEt,
|
||||||
|
localeEu,
|
||||||
|
localeFa,
|
||||||
|
localeFi,
|
||||||
|
localeFo,
|
||||||
|
localeFr_ch,
|
||||||
|
localeFr,
|
||||||
|
localeGl,
|
||||||
|
localeGu,
|
||||||
|
localeHe,
|
||||||
|
localeHi_in,
|
||||||
|
localeHr,
|
||||||
|
localeHu,
|
||||||
|
localeHy,
|
||||||
|
localeId,
|
||||||
|
localeIs,
|
||||||
|
localeIt,
|
||||||
|
localeJa,
|
||||||
|
localeKa,
|
||||||
|
localeKm,
|
||||||
|
localeKo,
|
||||||
|
localeLt,
|
||||||
|
localeLv,
|
||||||
|
localeMe_me,
|
||||||
|
localeMe,
|
||||||
|
localeMk,
|
||||||
|
localeMl,
|
||||||
|
localeMs,
|
||||||
|
localeMt,
|
||||||
|
localeNl_be,
|
||||||
|
localeNl,
|
||||||
|
localeNo,
|
||||||
|
localePa,
|
||||||
|
localePl,
|
||||||
|
localePt_br,
|
||||||
|
localePt_pt,
|
||||||
|
localeRm,
|
||||||
|
localeRo,
|
||||||
|
localeRu,
|
||||||
|
localeSk,
|
||||||
|
localeSl,
|
||||||
|
localeSq,
|
||||||
|
localeSr_sr,
|
||||||
|
localeSr,
|
||||||
|
localeSv,
|
||||||
|
localeSw,
|
||||||
|
localeTa,
|
||||||
|
localeTh,
|
||||||
|
localeTr,
|
||||||
|
localeTt,
|
||||||
|
localeUk,
|
||||||
|
localeUr,
|
||||||
|
localeVi,
|
||||||
|
localeZh_cn,
|
||||||
|
localeZh_hk,
|
||||||
|
localeZh_tw,
|
||||||
|
]);
|
||||||
@@ -26,9 +26,13 @@ function getHoverInfoPattern(options: any) {
|
|||||||
|
|
||||||
function prepareBarSeries(series: any, options: any, additionalOptions: any) {
|
function prepareBarSeries(series: any, options: any, additionalOptions: any) {
|
||||||
series.type = "bar";
|
series.type = "bar";
|
||||||
series.offsetgroup = toString(additionalOptions.index);
|
if (!options.series.stacking) {
|
||||||
|
series.offsetgroup = toString(additionalOptions.index);
|
||||||
|
}
|
||||||
if (options.showDataLabels) {
|
if (options.showDataLabels) {
|
||||||
series.textposition = "inside";
|
series.textposition = "inside";
|
||||||
|
} else {
|
||||||
|
series.textposition = "none";
|
||||||
}
|
}
|
||||||
return series;
|
return series;
|
||||||
}
|
}
|
||||||
@@ -92,7 +96,10 @@ function prepareSeries(series: any, options: any, numSeries: any, additionalOpti
|
|||||||
// For bubble/scatter charts `y` may be any (similar to `x`) - numeric is only bubble size;
|
// For bubble/scatter charts `y` may be any (similar to `x`) - numeric is only bubble size;
|
||||||
// for other types `y` is always number
|
// for other types `y` is always number
|
||||||
const cleanYValue = includes(["bubble", "scatter"], seriesOptions.type)
|
const cleanYValue = includes(["bubble", "scatter"], seriesOptions.type)
|
||||||
? normalizeValue
|
? (v: any, axixType: any) => {
|
||||||
|
v = normalizeValue(v, axixType);
|
||||||
|
return includes(["scatter"], seriesOptions.type) && options.missingValuesAsZero && isNil(v) ? 0.0 : v;
|
||||||
|
}
|
||||||
: (v: any) => {
|
: (v: any) => {
|
||||||
v = cleanNumber(v);
|
v = cleanNumber(v);
|
||||||
return options.missingValuesAsZero && isNil(v) ? 0.0 : v;
|
return options.missingValuesAsZero && isNil(v) ? 0.0 : v;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default {
|
|||||||
Renderer,
|
Renderer,
|
||||||
Editor,
|
Editor,
|
||||||
|
|
||||||
defaultColumns: 3,
|
defaultColumns: 6,
|
||||||
defaultRows: 8,
|
defaultRows: 8,
|
||||||
minColumns: 2,
|
minColumns: 2,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,6 @@ export default {
|
|||||||
Renderer,
|
Renderer,
|
||||||
Editor,
|
Editor,
|
||||||
|
|
||||||
defaultColumns: 2,
|
defaultColumns: 4,
|
||||||
defaultRows: 5,
|
defaultRows: 5,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ export default {
|
|||||||
...options,
|
...options,
|
||||||
}),
|
}),
|
||||||
Renderer: DetailsRenderer,
|
Renderer: DetailsRenderer,
|
||||||
defaultColumns: 2,
|
defaultColumns: 4,
|
||||||
defaultRows: 2,
|
defaultRows: 2,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default {
|
|||||||
Renderer,
|
Renderer,
|
||||||
Editor,
|
Editor,
|
||||||
|
|
||||||
defaultColumns: 3,
|
defaultColumns: 6,
|
||||||
defaultRows: 8,
|
defaultRows: 8,
|
||||||
minColumns: 2,
|
minColumns: 2,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,6 @@ export default {
|
|||||||
Editor,
|
Editor,
|
||||||
|
|
||||||
defaultRows: 10,
|
defaultRows: 10,
|
||||||
defaultColumns: 3,
|
defaultColumns: 6,
|
||||||
minColumns: 2,
|
minColumns: 2,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Object {
|
|||||||
"linkTitleTemplate": "{{ @ }}",
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
"linkUrlTemplate": "{{ @ }}",
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
"name": "a",
|
"name": "a",
|
||||||
|
"nullValue": "null",
|
||||||
"numberFormat": undefined,
|
"numberFormat": undefined,
|
||||||
"order": 100000,
|
"order": 100000,
|
||||||
"title": "a",
|
"title": "a",
|
||||||
@@ -56,6 +57,7 @@ Object {
|
|||||||
"linkTitleTemplate": "{{ @ }}",
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
"linkUrlTemplate": "{{ @ }}",
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
"name": "a",
|
"name": "a",
|
||||||
|
"nullValue": "null",
|
||||||
"numberFormat": undefined,
|
"numberFormat": undefined,
|
||||||
"order": 100000,
|
"order": 100000,
|
||||||
"title": "a",
|
"title": "a",
|
||||||
@@ -89,6 +91,7 @@ Object {
|
|||||||
"linkTitleTemplate": "{{ @ }}",
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
"linkUrlTemplate": "{{ @ }}",
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
"name": "a",
|
"name": "a",
|
||||||
|
"nullValue": "null",
|
||||||
"numberFormat": undefined,
|
"numberFormat": undefined,
|
||||||
"order": 100000,
|
"order": 100000,
|
||||||
"title": "test",
|
"title": "test",
|
||||||
@@ -122,6 +125,7 @@ Object {
|
|||||||
"linkTitleTemplate": "{{ @ }}",
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
"linkUrlTemplate": "{{ @ }}",
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
"name": "a",
|
"name": "a",
|
||||||
|
"nullValue": "null",
|
||||||
"numberFormat": undefined,
|
"numberFormat": undefined,
|
||||||
"order": 100000,
|
"order": 100000,
|
||||||
"title": "a",
|
"title": "a",
|
||||||
@@ -155,6 +159,7 @@ Object {
|
|||||||
"linkTitleTemplate": "{{ @ }}",
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
"linkUrlTemplate": "{{ @ }}",
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
"name": "a",
|
"name": "a",
|
||||||
|
"nullValue": "null",
|
||||||
"numberFormat": undefined,
|
"numberFormat": undefined,
|
||||||
"order": 100000,
|
"order": 100000,
|
||||||
"title": "a",
|
"title": "a",
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ function Editor({ column, onChange }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function initNumberColumn(column: any) {
|
export default function initNumberColumn(column: any) {
|
||||||
const format = createNumberFormatter(column.numberFormat);
|
const format = createNumberFormatter(column.numberFormat, true);
|
||||||
|
|
||||||
function prepareData(row: any) {
|
function prepareData(row: any) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default function initTextColumn(column: any) {
|
|||||||
function TextColumn({ row }: any) {
|
function TextColumn({ row }: any) {
|
||||||
// eslint-disable-line react/prop-types
|
// eslint-disable-line react/prop-types
|
||||||
const { text } = prepareData(row);
|
const { text } = prepareData(row);
|
||||||
return column.allowHTML ? <HtmlContent>{text}</HtmlContent> : text;
|
return (column.allowHTML && typeof text === 'string') ? <HtmlContent>{text}</HtmlContent> : text;
|
||||||
}
|
}
|
||||||
|
|
||||||
TextColumn.prepareData = prepareData;
|
TextColumn.prepareData = prepareData;
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ function getDefaultFormatOptions(column: any) {
|
|||||||
dateTimeFormat: dateTimeFormat[column.type],
|
dateTimeFormat: dateTimeFormat[column.type],
|
||||||
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
||||||
numberFormat: numberFormat[column.type],
|
numberFormat: numberFormat[column.type],
|
||||||
|
nullValue: visualizationsSettings.nullValue,
|
||||||
booleanValues: visualizationsSettings.booleanValues || ["false", "true"],
|
booleanValues: visualizationsSettings.booleanValues || ["false", "true"],
|
||||||
// `image` cell options
|
// `image` cell options
|
||||||
imageUrlTemplate: "{{ @ }}",
|
imageUrlTemplate: "{{ @ }}",
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ export default {
|
|||||||
|
|
||||||
autoHeight: true,
|
autoHeight: true,
|
||||||
defaultRows: 14,
|
defaultRows: 14,
|
||||||
defaultColumns: 3,
|
defaultColumns: 6,
|
||||||
minColumns: 2,
|
minColumns: 2,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,6 +39,11 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.display-as-null {
|
||||||
|
font-style: italic;
|
||||||
|
color: @text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
.table-visualization-spacer {
|
.table-visualization-spacer {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const visualizationsSettings = {
|
|||||||
dateTimeFormat: "DD/MM/YYYY HH:mm",
|
dateTimeFormat: "DD/MM/YYYY HH:mm",
|
||||||
integerFormat: "0,0",
|
integerFormat: "0,0",
|
||||||
floatFormat: "0,0.00",
|
floatFormat: "0,0.00",
|
||||||
|
nullValue: "null",
|
||||||
booleanValues: ["false", "true"],
|
booleanValues: ["false", "true"],
|
||||||
tableCellMaxJSONSize: 50000,
|
tableCellMaxJSONSize: 50000,
|
||||||
allowCustomJSVisualizations: false,
|
allowCustomJSVisualizations: false,
|
||||||
|
|||||||
@@ -1776,6 +1776,11 @@
|
|||||||
parse-rect "^1.2.0"
|
parse-rect "^1.2.0"
|
||||||
pick-by-alias "^1.2.0"
|
pick-by-alias "^1.2.0"
|
||||||
|
|
||||||
|
"@plotly/regl@^2.1.2":
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@plotly/regl/-/regl-2.1.2.tgz#fd31e3e820ed8824d59a67ab5e766bb101b810b6"
|
||||||
|
integrity sha512-Mdk+vUACbQvjd0m/1JJjOOafmkp/EpmHjISsopEz5Av44CBq7rPC05HHNbYGKVyNUF2zmEoBS/TT0pd0SPFFyw==
|
||||||
|
|
||||||
"@ts-morph/bootstrap@^0.16.0":
|
"@ts-morph/bootstrap@^0.16.0":
|
||||||
version "0.16.0"
|
version "0.16.0"
|
||||||
resolved "https://registry.yarnpkg.com/@ts-morph/bootstrap/-/bootstrap-0.16.0.tgz#c97034175a8fc2b7d3f575526d819877f7ed2d83"
|
resolved "https://registry.yarnpkg.com/@ts-morph/bootstrap/-/bootstrap-0.16.0.tgz#c97034175a8fc2b7d3f575526d819877f7ed2d83"
|
||||||
@@ -2229,10 +2234,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/pbf/-/pbf-3.0.5.tgz#a9495a58d8c75be4ffe9a0bd749a307715c07404"
|
resolved "https://registry.yarnpkg.com/@types/pbf/-/pbf-3.0.5.tgz#a9495a58d8c75be4ffe9a0bd749a307715c07404"
|
||||||
integrity sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==
|
integrity sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==
|
||||||
|
|
||||||
"@types/plotly.js@^2.35.2":
|
"@types/plotly.js@^3.0.3":
|
||||||
version "2.35.2"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/plotly.js/-/plotly.js-2.35.2.tgz#c771a7bf56b398730f8c799879895de4294289c0"
|
resolved "https://registry.yarnpkg.com/@types/plotly.js/-/plotly.js-3.0.3.tgz#ed72b67ce65adc22b2bc75da845e973335ea7234"
|
||||||
integrity sha512-tn0Kp7F6VWiu96jknCvR/PcdIGIATeIK+Z5WXH3bEvG6CRwUNfhy34yBhfPYmTea7mMQxXvTZKGMm6/Y4wxESg==
|
integrity sha512-9CENH8hh2diOML3o4lEd4H0nwQ4uECEE9mZQc+zriGEdd0zK8ru75t7qFhaMQmiWFFPGWqI4FpodBZFTmWpdbQ==
|
||||||
|
|
||||||
"@types/prop-types@*":
|
"@types/prop-types@*":
|
||||||
version "15.7.3"
|
version "15.7.3"
|
||||||
@@ -3299,6 +3304,11 @@ color-name@1.1.3, color-name@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||||
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
||||||
|
|
||||||
|
color-name@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-2.0.0.tgz#03ff6b1b5aec9bb3cf1ed82400c2790dfcd01d2d"
|
||||||
|
integrity sha512-SbtvAMWvASO5TE2QP07jHBMXKafgdZz8Vrsrn96fiL+O92/FN/PLARzUW5sKt013fjAprK2d2iCn2hk2Xb5oow==
|
||||||
|
|
||||||
color-name@~1.1.4:
|
color-name@~1.1.4:
|
||||||
version "1.1.4"
|
version "1.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||||
@@ -3329,7 +3339,22 @@ color-parse@^1.3.8:
|
|||||||
defined "^1.0.0"
|
defined "^1.0.0"
|
||||||
is-plain-obj "^1.1.0"
|
is-plain-obj "^1.1.0"
|
||||||
|
|
||||||
color-rgba@2.1.1, color-rgba@^2.1.1:
|
color-parse@^2.0.0:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-parse/-/color-parse-2.0.2.tgz#37b46930424924060988edf25b24e6ffb4a1dc3f"
|
||||||
|
integrity sha512-eCtOz5w5ttWIUcaKLiktF+DxZO1R9KLNY/xhbV6CkhM7sR3GhVghmt6X6yOnzeaM24po+Z9/S1apbXMwA3Iepw==
|
||||||
|
dependencies:
|
||||||
|
color-name "^2.0.0"
|
||||||
|
|
||||||
|
color-rgba@3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-rgba/-/color-rgba-3.0.0.tgz#77090bdcdb2951c1735e20099ddd50401675375b"
|
||||||
|
integrity sha512-PPwZYkEY3M2THEHHV6Y95sGUie77S7X8v+h1r6LSAPF3/LL2xJ8duUXSrkic31Nzc4odPwHgUbiX/XuTYzQHQg==
|
||||||
|
dependencies:
|
||||||
|
color-parse "^2.0.0"
|
||||||
|
color-space "^2.0.0"
|
||||||
|
|
||||||
|
color-rgba@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/color-rgba/-/color-rgba-2.1.1.tgz#4633b83817c7406c90b3d7bf4d1acfa48dde5c83"
|
resolved "https://registry.yarnpkg.com/color-rgba/-/color-rgba-2.1.1.tgz#4633b83817c7406c90b3d7bf4d1acfa48dde5c83"
|
||||||
integrity sha512-VaX97wsqrMwLSOR6H7rU1Doa2zyVdmShabKrPEIFywLlHoibgD3QW9Dw6fSqM4+H/LfjprDNAUUW31qEQcGzNw==
|
integrity sha512-VaX97wsqrMwLSOR6H7rU1Doa2zyVdmShabKrPEIFywLlHoibgD3QW9Dw6fSqM4+H/LfjprDNAUUW31qEQcGzNw==
|
||||||
@@ -3346,6 +3371,11 @@ color-space@^1.14.6:
|
|||||||
hsluv "^0.0.3"
|
hsluv "^0.0.3"
|
||||||
mumath "^3.3.4"
|
mumath "^3.3.4"
|
||||||
|
|
||||||
|
color-space@^2.0.0:
|
||||||
|
version "2.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-space/-/color-space-2.3.2.tgz#d8c72bab09ef26b98abebc58bc1586ce3073033d"
|
||||||
|
integrity sha512-BcKnbOEsOarCwyoLstcoEztwT0IJxqqQkNwDuA3a65sICvvHL2yoeV13psoDFh5IuiOMnIOKdQDwB4Mk3BypiA==
|
||||||
|
|
||||||
colorette@^2.0.14:
|
colorette@^2.0.14:
|
||||||
version "2.0.20"
|
version "2.0.20"
|
||||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
|
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
|
||||||
@@ -3532,20 +3562,6 @@ css-loader@^3.5.2:
|
|||||||
schema-utils "^2.6.5"
|
schema-utils "^2.6.5"
|
||||||
semver "^6.3.0"
|
semver "^6.3.0"
|
||||||
|
|
||||||
css-loader@^7.1.2:
|
|
||||||
version "7.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-7.1.2.tgz#64671541c6efe06b0e22e750503106bdd86880f8"
|
|
||||||
integrity sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==
|
|
||||||
dependencies:
|
|
||||||
icss-utils "^5.1.0"
|
|
||||||
postcss "^8.4.33"
|
|
||||||
postcss-modules-extract-imports "^3.1.0"
|
|
||||||
postcss-modules-local-by-default "^4.0.5"
|
|
||||||
postcss-modules-scope "^3.2.0"
|
|
||||||
postcss-modules-values "^4.0.0"
|
|
||||||
postcss-value-parser "^4.2.0"
|
|
||||||
semver "^7.5.4"
|
|
||||||
|
|
||||||
css-select@~1.2.0:
|
css-select@~1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
|
resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
|
||||||
@@ -5337,11 +5353,6 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
postcss "^7.0.14"
|
postcss "^7.0.14"
|
||||||
|
|
||||||
icss-utils@^5.0.0, icss-utils@^5.1.0:
|
|
||||||
version "5.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
|
|
||||||
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
|
|
||||||
|
|
||||||
ieee754@^1.1.12:
|
ieee754@^1.1.12:
|
||||||
version "1.1.13"
|
version "1.1.13"
|
||||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
|
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
|
||||||
@@ -6693,7 +6704,7 @@ map-visit@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
object-visit "^1.0.0"
|
object-visit "^1.0.0"
|
||||||
|
|
||||||
maplibre-gl@^4.5.2:
|
maplibre-gl@^4.7.1:
|
||||||
version "4.7.1"
|
version "4.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/maplibre-gl/-/maplibre-gl-4.7.1.tgz#06a524438ee2aafbe8bcd91002a4e01468ea5486"
|
resolved "https://registry.yarnpkg.com/maplibre-gl/-/maplibre-gl-4.7.1.tgz#06a524438ee2aafbe8bcd91002a4e01468ea5486"
|
||||||
integrity sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==
|
integrity sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==
|
||||||
@@ -6896,11 +6907,6 @@ nan@^2.12.1:
|
|||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
|
||||||
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
|
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
|
||||||
|
|
||||||
nanoid@^3.3.8:
|
|
||||||
version "3.3.8"
|
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
|
|
||||||
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
|
|
||||||
|
|
||||||
nanomatch@^1.2.9:
|
nanomatch@^1.2.9:
|
||||||
version "1.2.13"
|
version "1.2.13"
|
||||||
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
|
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
|
||||||
@@ -7447,11 +7453,6 @@ picocolors@^1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1"
|
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1"
|
||||||
integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
|
integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
|
||||||
|
|
||||||
picocolors@^1.1.1:
|
|
||||||
version "1.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
|
||||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
|
||||||
|
|
||||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
|
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||||
@@ -7505,15 +7506,16 @@ pkg-up@^3.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
find-up "^3.0.0"
|
find-up "^3.0.0"
|
||||||
|
|
||||||
plotly.js@2.35.3:
|
plotly.js@3.1.0:
|
||||||
version "2.35.3"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/plotly.js/-/plotly.js-2.35.3.tgz#6a7787d63b4d334948c281aa9c8df7fb941b425e"
|
resolved "https://registry.yarnpkg.com/plotly.js/-/plotly.js-3.1.0.tgz#a095a37d0f1b04bb0e9686853df54a4e6437af2e"
|
||||||
integrity sha512-7RaC6FxmCUhpD6H4MpD+QLUu3hCn76I11rotRefrh3m1iDvWqGnVqVk9dSaKmRAhFD3vsNsYea0OxnR1rc2IzQ==
|
integrity sha512-vx+CyzApL9tquFpwoPHOGSIWDbFPsA4om/tXZcnsygGUejXideDF9R5VwkltEIDG7Xuof45quVPyz1otv6Aqjw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@plotly/d3" "3.8.2"
|
"@plotly/d3" "3.8.2"
|
||||||
"@plotly/d3-sankey" "0.7.2"
|
"@plotly/d3-sankey" "0.7.2"
|
||||||
"@plotly/d3-sankey-circular" "0.33.1"
|
"@plotly/d3-sankey-circular" "0.33.1"
|
||||||
"@plotly/mapbox-gl" "1.13.4"
|
"@plotly/mapbox-gl" "1.13.4"
|
||||||
|
"@plotly/regl" "^2.1.2"
|
||||||
"@turf/area" "^7.1.0"
|
"@turf/area" "^7.1.0"
|
||||||
"@turf/bbox" "^7.1.0"
|
"@turf/bbox" "^7.1.0"
|
||||||
"@turf/centroid" "^7.1.0"
|
"@turf/centroid" "^7.1.0"
|
||||||
@@ -7522,9 +7524,8 @@ plotly.js@2.35.3:
|
|||||||
color-alpha "1.0.4"
|
color-alpha "1.0.4"
|
||||||
color-normalize "1.5.0"
|
color-normalize "1.5.0"
|
||||||
color-parse "2.0.0"
|
color-parse "2.0.0"
|
||||||
color-rgba "2.1.1"
|
color-rgba "3.0.0"
|
||||||
country-regex "^1.1.0"
|
country-regex "^1.1.0"
|
||||||
css-loader "^7.1.2"
|
|
||||||
d3-force "^1.2.1"
|
d3-force "^1.2.1"
|
||||||
d3-format "^1.4.5"
|
d3-format "^1.4.5"
|
||||||
d3-geo "^1.12.1"
|
d3-geo "^1.12.1"
|
||||||
@@ -7539,7 +7540,7 @@ plotly.js@2.35.3:
|
|||||||
has-hover "^1.0.1"
|
has-hover "^1.0.1"
|
||||||
has-passive-events "^1.0.0"
|
has-passive-events "^1.0.0"
|
||||||
is-mobile "^4.0.0"
|
is-mobile "^4.0.0"
|
||||||
maplibre-gl "^4.5.2"
|
maplibre-gl "^4.7.1"
|
||||||
mouse-change "^1.4.0"
|
mouse-change "^1.4.0"
|
||||||
mouse-event-offset "^3.0.2"
|
mouse-event-offset "^3.0.2"
|
||||||
mouse-wheel "^1.2.0"
|
mouse-wheel "^1.2.0"
|
||||||
@@ -7548,20 +7549,18 @@ plotly.js@2.35.3:
|
|||||||
point-in-polygon "^1.1.0"
|
point-in-polygon "^1.1.0"
|
||||||
polybooljs "^1.2.2"
|
polybooljs "^1.2.2"
|
||||||
probe-image-size "^7.2.3"
|
probe-image-size "^7.2.3"
|
||||||
regl "npm:@plotly/regl@^2.1.2"
|
|
||||||
regl-error2d "^2.0.12"
|
regl-error2d "^2.0.12"
|
||||||
regl-line2d "^3.1.3"
|
regl-line2d "^3.1.3"
|
||||||
regl-scatter2d "^3.3.1"
|
regl-scatter2d "^3.3.1"
|
||||||
regl-splom "^1.0.14"
|
regl-splom "^1.0.14"
|
||||||
strongly-connected-components "^1.0.1"
|
strongly-connected-components "^1.0.1"
|
||||||
style-loader "^4.0.0"
|
|
||||||
superscript-text "^1.0.0"
|
superscript-text "^1.0.0"
|
||||||
svg-path-sdf "^1.1.3"
|
svg-path-sdf "^1.1.3"
|
||||||
tinycolor2 "^1.4.2"
|
tinycolor2 "^1.4.2"
|
||||||
to-px "1.0.1"
|
to-px "1.0.1"
|
||||||
topojson-client "^3.1.0"
|
topojson-client "^3.1.0"
|
||||||
webgl-context "^2.2.0"
|
webgl-context "^2.2.0"
|
||||||
world-calendars "^1.0.3"
|
world-calendars "^1.0.4"
|
||||||
|
|
||||||
pn@^1.1.0:
|
pn@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
@@ -7590,11 +7589,6 @@ postcss-modules-extract-imports@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
postcss "^7.0.5"
|
postcss "^7.0.5"
|
||||||
|
|
||||||
postcss-modules-extract-imports@^3.1.0:
|
|
||||||
version "3.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002"
|
|
||||||
integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==
|
|
||||||
|
|
||||||
postcss-modules-local-by-default@^3.0.2:
|
postcss-modules-local-by-default@^3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz#e8a6561be914aaf3c052876377524ca90dbb7915"
|
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz#e8a6561be914aaf3c052876377524ca90dbb7915"
|
||||||
@@ -7605,15 +7599,6 @@ postcss-modules-local-by-default@^3.0.2:
|
|||||||
postcss-selector-parser "^6.0.2"
|
postcss-selector-parser "^6.0.2"
|
||||||
postcss-value-parser "^4.0.0"
|
postcss-value-parser "^4.0.0"
|
||||||
|
|
||||||
postcss-modules-local-by-default@^4.0.5:
|
|
||||||
version "4.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz#d150f43837831dae25e4085596e84f6f5d6ec368"
|
|
||||||
integrity sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==
|
|
||||||
dependencies:
|
|
||||||
icss-utils "^5.0.0"
|
|
||||||
postcss-selector-parser "^7.0.0"
|
|
||||||
postcss-value-parser "^4.1.0"
|
|
||||||
|
|
||||||
postcss-modules-scope@^2.2.0:
|
postcss-modules-scope@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee"
|
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee"
|
||||||
@@ -7622,13 +7607,6 @@ postcss-modules-scope@^2.2.0:
|
|||||||
postcss "^7.0.6"
|
postcss "^7.0.6"
|
||||||
postcss-selector-parser "^6.0.0"
|
postcss-selector-parser "^6.0.0"
|
||||||
|
|
||||||
postcss-modules-scope@^3.2.0:
|
|
||||||
version "3.2.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz#1bbccddcb398f1d7a511e0a2d1d047718af4078c"
|
|
||||||
integrity sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==
|
|
||||||
dependencies:
|
|
||||||
postcss-selector-parser "^7.0.0"
|
|
||||||
|
|
||||||
postcss-modules-values@^3.0.0:
|
postcss-modules-values@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10"
|
resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10"
|
||||||
@@ -7637,13 +7615,6 @@ postcss-modules-values@^3.0.0:
|
|||||||
icss-utils "^4.0.0"
|
icss-utils "^4.0.0"
|
||||||
postcss "^7.0.6"
|
postcss "^7.0.6"
|
||||||
|
|
||||||
postcss-modules-values@^4.0.0:
|
|
||||||
version "4.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c"
|
|
||||||
integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==
|
|
||||||
dependencies:
|
|
||||||
icss-utils "^5.0.0"
|
|
||||||
|
|
||||||
postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
|
postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
|
||||||
version "6.0.2"
|
version "6.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
|
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
|
||||||
@@ -7653,14 +7624,6 @@ postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
|
|||||||
indexes-of "^1.0.1"
|
indexes-of "^1.0.1"
|
||||||
uniq "^1.0.1"
|
uniq "^1.0.1"
|
||||||
|
|
||||||
postcss-selector-parser@^7.0.0:
|
|
||||||
version "7.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz#4d6af97eba65d73bc4d84bcb343e865d7dd16262"
|
|
||||||
integrity sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==
|
|
||||||
dependencies:
|
|
||||||
cssesc "^3.0.0"
|
|
||||||
util-deprecate "^1.0.2"
|
|
||||||
|
|
||||||
postcss-value-parser@^3.2.3:
|
postcss-value-parser@^3.2.3:
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
|
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
|
||||||
@@ -7671,11 +7634,6 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz#651ff4593aa9eda8d5d0d66593a2417aeaeb325d"
|
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz#651ff4593aa9eda8d5d0d66593a2417aeaeb325d"
|
||||||
integrity sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg==
|
integrity sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg==
|
||||||
|
|
||||||
postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
|
||||||
version "4.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
|
||||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
|
||||||
|
|
||||||
postcss@^6.0.22, postcss@^6.0.23:
|
postcss@^6.0.22, postcss@^6.0.23:
|
||||||
version "6.0.23"
|
version "6.0.23"
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
|
||||||
@@ -7694,15 +7652,6 @@ postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.27, postcss@^7.0.5, postcss@^7.0.
|
|||||||
source-map "^0.6.1"
|
source-map "^0.6.1"
|
||||||
supports-color "^6.1.0"
|
supports-color "^6.1.0"
|
||||||
|
|
||||||
postcss@^8.4.33:
|
|
||||||
version "8.5.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb"
|
|
||||||
integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==
|
|
||||||
dependencies:
|
|
||||||
nanoid "^3.3.8"
|
|
||||||
picocolors "^1.1.1"
|
|
||||||
source-map-js "^1.2.1"
|
|
||||||
|
|
||||||
potpack@^1.0.1:
|
potpack@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/potpack/-/potpack-1.0.1.tgz#d1b1afd89e4c8f7762865ec30bd112ab767e2ebf"
|
resolved "https://registry.yarnpkg.com/potpack/-/potpack-1.0.1.tgz#d1b1afd89e4c8f7762865ec30bd112ab767e2ebf"
|
||||||
@@ -7723,10 +7672,10 @@ prelude-ls@~1.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
||||||
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
|
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
|
||||||
|
|
||||||
prettier@^1.19.1:
|
prettier@3.3.2:
|
||||||
version "1.19.1"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a"
|
||||||
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==
|
||||||
|
|
||||||
pretty-format@^24.9.0:
|
pretty-format@^24.9.0:
|
||||||
version "24.9.0"
|
version "24.9.0"
|
||||||
@@ -8231,11 +8180,6 @@ regl@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/regl/-/regl-2.1.1.tgz#fb3eecbc684031ec6172f68aaab2cbe9c3aa3148"
|
resolved "https://registry.yarnpkg.com/regl/-/regl-2.1.1.tgz#fb3eecbc684031ec6172f68aaab2cbe9c3aa3148"
|
||||||
integrity sha512-+IOGrxl3FZ8ZM9ixCWQZzFRiRn7Rzn9bu3iFHwg/yz4tlOUQgbO4PHLgG+1ZT60zcIV8tief6Qrmyl8qcoJP0g==
|
integrity sha512-+IOGrxl3FZ8ZM9ixCWQZzFRiRn7Rzn9bu3iFHwg/yz4tlOUQgbO4PHLgG+1ZT60zcIV8tief6Qrmyl8qcoJP0g==
|
||||||
|
|
||||||
"regl@npm:@plotly/regl@^2.1.2":
|
|
||||||
version "2.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@plotly/regl/-/regl-2.1.2.tgz#fd31e3e820ed8824d59a67ab5e766bb101b810b6"
|
|
||||||
integrity sha512-Mdk+vUACbQvjd0m/1JJjOOafmkp/EpmHjISsopEz5Av44CBq7rPC05HHNbYGKVyNUF2zmEoBS/TT0pd0SPFFyw==
|
|
||||||
|
|
||||||
remove-trailing-separator@^1.0.1:
|
remove-trailing-separator@^1.0.1:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
|
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
|
||||||
@@ -8559,11 +8503,6 @@ semver@^7.2.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lru-cache "^6.0.0"
|
lru-cache "^6.0.0"
|
||||||
|
|
||||||
semver@^7.5.4:
|
|
||||||
version "7.7.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
|
|
||||||
integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
|
|
||||||
|
|
||||||
serialize-javascript@^6.0.1:
|
serialize-javascript@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c"
|
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c"
|
||||||
@@ -8705,11 +8644,6 @@ sortablejs@^1.6.1:
|
|||||||
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.2.tgz#6e40364d913f98b85a14f6678f92b5c1221f5290"
|
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.2.tgz#6e40364d913f98b85a14f6678f92b5c1221f5290"
|
||||||
integrity sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==
|
integrity sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==
|
||||||
|
|
||||||
source-map-js@^1.2.1:
|
|
||||||
version "1.2.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
|
||||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
|
||||||
|
|
||||||
source-map-resolve@^0.5.0:
|
source-map-resolve@^0.5.0:
|
||||||
version "0.5.3"
|
version "0.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
|
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
|
||||||
@@ -9000,11 +8934,6 @@ style-loader@^3.3.3:
|
|||||||
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.3.tgz#bba8daac19930169c0c9c96706749a597ae3acff"
|
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.3.tgz#bba8daac19930169c0c9c96706749a597ae3acff"
|
||||||
integrity sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==
|
integrity sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==
|
||||||
|
|
||||||
style-loader@^4.0.0:
|
|
||||||
version "4.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-4.0.0.tgz#0ea96e468f43c69600011e0589cb05c44f3b17a5"
|
|
||||||
integrity sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==
|
|
||||||
|
|
||||||
supercluster@^7.1.0:
|
supercluster@^7.1.0:
|
||||||
version "7.1.5"
|
version "7.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-7.1.5.tgz#65a6ce4a037a972767740614c19051b64b8be5a3"
|
resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-7.1.5.tgz#65a6ce4a037a972767740614c19051b64b8be5a3"
|
||||||
@@ -9563,7 +9492,7 @@ use@^3.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
||||||
integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
|
integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
|
||||||
|
|
||||||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||||
@@ -9819,10 +9748,10 @@ word-wrap@~1.2.3:
|
|||||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||||
|
|
||||||
world-calendars@^1.0.3:
|
world-calendars@^1.0.4:
|
||||||
version "1.0.3"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/world-calendars/-/world-calendars-1.0.3.tgz#b25c5032ba24128ffc41d09faf4a5ec1b9c14335"
|
resolved "https://registry.yarnpkg.com/world-calendars/-/world-calendars-1.0.4.tgz#2a12bcbd796b6c99aef2e52f281229faad8fa96c"
|
||||||
integrity sha1-slxQMrokEo/8QdCfr0pewbnBQzU=
|
integrity sha512-VGRnLJS+xJmGDPodgJRnGIDwGu0s+Cr9V2HB3EzlDZ5n0qb8h5SJtGUEkjrphZYAglEiXZ6kiXdmk0H/h/uu/w==
|
||||||
dependencies:
|
dependencies:
|
||||||
object-assign "^4.1.0"
|
object-assign "^4.1.0"
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
const WebpackBuildNotifierPlugin = require("webpack-build-notifier");
|
const WebpackBuildNotifierPlugin = require("webpack-build-notifier");
|
||||||
const ManifestPlugin = require("webpack-manifest-plugin");
|
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
|
||||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
const LessPluginAutoPrefix = require("less-plugin-autoprefix");
|
const LessPluginAutoPrefix = require("less-plugin-autoprefix");
|
||||||
@@ -76,8 +76,6 @@ const config = {
|
|||||||
publicPath: staticPath
|
publicPath: staticPath
|
||||||
},
|
},
|
||||||
node: {
|
node: {
|
||||||
fs: "empty",
|
|
||||||
path: "empty"
|
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
symlinks: false,
|
symlinks: false,
|
||||||
@@ -85,6 +83,14 @@ const config = {
|
|||||||
alias: {
|
alias: {
|
||||||
"@": appPath,
|
"@": appPath,
|
||||||
extensions: extensionPath
|
extensions: extensionPath
|
||||||
|
},
|
||||||
|
fallback: {
|
||||||
|
fs: false,
|
||||||
|
url: require.resolve("url/"),
|
||||||
|
stream: require.resolve("stream-browserify"),
|
||||||
|
assert: require.resolve("assert/"),
|
||||||
|
util: require.resolve("util/"),
|
||||||
|
process: require.resolve("process/browser"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -109,7 +115,7 @@ const config = {
|
|||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: "[name].[chunkhash].css"
|
filename: "[name].[chunkhash].css"
|
||||||
}),
|
}),
|
||||||
new ManifestPlugin({
|
new WebpackManifestPlugin({
|
||||||
fileName: "asset-manifest.json",
|
fileName: "asset-manifest.json",
|
||||||
publicPath: ""
|
publicPath: ""
|
||||||
}),
|
}),
|
||||||
@@ -122,7 +128,13 @@ const config = {
|
|||||||
{ from: "client/app/assets/fonts", to: "fonts/" }
|
{ from: "client/app/assets/fonts", to: "fonts/" }
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
isHotReloadingEnabled && new ReactRefreshWebpackPlugin({ overlay: false })
|
isHotReloadingEnabled && new ReactRefreshWebpackPlugin({ overlay: false }),
|
||||||
|
new webpack.ProvidePlugin({
|
||||||
|
// Make a global `process` variable that points to the `process` package,
|
||||||
|
// because the `util` package expects there to be a global variable named `process`.
|
||||||
|
// Thanks to https://stackoverflow.com/a/65018686/14239942
|
||||||
|
process: 'process/browser'
|
||||||
|
})
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
optimization: {
|
optimization: {
|
||||||
splitChunks: {
|
splitChunks: {
|
||||||
@@ -133,6 +145,17 @@ const config = {
|
|||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
enforce: "pre",
|
||||||
|
use: ["source-map-loader"],
|
||||||
|
resolve: {
|
||||||
|
fullySpecified: false
|
||||||
|
},
|
||||||
|
exclude: [
|
||||||
|
/node_modules\/@plotly\/mapbox-gl/,
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.(t|j)sx?$/,
|
test: /\.(t|j)sx?$/,
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
@@ -228,7 +251,7 @@ const config = {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
devtool: isProduction ? "source-map" : "cheap-eval-module-source-map",
|
devtool: isProduction ? "source-map" : "eval-cheap-module-source-map",
|
||||||
stats: {
|
stats: {
|
||||||
children: false,
|
children: false,
|
||||||
modules: false,
|
modules: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user