diff --git a/atopcalcite/.settings/org.eclipse.core.resources.prefs b/atopcalcite/.settings/org.eclipse.core.resources.prefs index 29abf9995..989609020 100644 --- a/atopcalcite/.settings/org.eclipse.core.resources.prefs +++ b/atopcalcite/.settings/org.eclipse.core.resources.prefs @@ -1,6 +1,5 @@ eclipse.preferences.version=1 encoding//src/main/java=UTF-8 encoding//src/main/resources=UTF-8 -encoding//src/test/java=UTF-8 encoding//src/test/resources=UTF-8 encoding/=UTF-8 diff --git a/atopcalcite/src/main/java/net/hydromatic/optiq/runtime/SqlFunctions.java b/atopcalcite/src/main/java/net/hydromatic/optiq/runtime/SqlFunctions.java index f9ba797d7..89500c957 100644 --- a/atopcalcite/src/main/java/net/hydromatic/optiq/runtime/SqlFunctions.java +++ b/atopcalcite/src/main/java/net/hydromatic/optiq/runtime/SqlFunctions.java @@ -1,5 +1,5 @@ /* - * OVERRIDE POINTS: + * OVERRIDE POINT: * - divide(BigDecimal,BigDecimal), was `b0.divide(b1)`, now `b0.divide(b1, MathContext.DECIMAL64);` */ diff --git a/atopcalcite/src/main/java/org/eigenbase/sql2rel/SqlToRelConverter.java b/atopcalcite/src/main/java/org/eigenbase/sql2rel/SqlToRelConverter.java index 15077e052..796640cfe 100644 --- a/atopcalcite/src/main/java/org/eigenbase/sql2rel/SqlToRelConverter.java +++ b/atopcalcite/src/main/java/org/eigenbase/sql2rel/SqlToRelConverter.java @@ -1,7 +1,8 @@ /* - * OVERRIDE POINTS: + * OVERRIDE POINT: * - getInSubqueryThreshold(), was `20`, now `Integer.MAX_VALUE` * - isTrimUnusedFields(), override to false + * - AggConverter.visit(SqlCall), skip column reading for COUNT(COL), for https://jirap.corp.ebay.com/browse/KYLIN-104 */ /* @@ -4430,7 +4431,7 @@ public Void visit(SqlCall call) { // special case for COUNT(*): delete the * if (operand instanceof SqlIdentifier) { SqlIdentifier id = (SqlIdentifier) operand; - if (id.isStar()) { + if (id.isStar() || isSimpleCount(call)) { // OVERRIDE POINT, was just `id.isStar()` assert call.operandCount() == 1; assert args.isEmpty(); break; @@ -4487,6 +4488,18 @@ public Void visit(SqlCall call) { return null; } + // OVERRIDE POINT + private boolean isSimpleCount(SqlCall call) { + if (call.getOperator().isName("COUNT") && call.operandCount() == 1) { + final SqlNode parm = call.operand(0); + if ((parm instanceof SqlIdentifier || parm instanceof SqlNumericLiteral) // + && call.getFunctionQuantifier() == null) { + return true; + } + } + return false; + } + private int lookupOrCreateGroupExpr(RexNode expr) { for (int i = 0; i < convertedInputExprs.size(); i++) { RexNode convertedInputExpr = convertedInputExprs.get(i); diff --git a/common/src/main/java/com/kylinolap/common/KylinConfig.java b/common/src/main/java/com/kylinolap/common/KylinConfig.java index 34b348ddc..0a7dcd5a7 100644 --- a/common/src/main/java/com/kylinolap/common/KylinConfig.java +++ b/common/src/main/java/com/kylinolap/common/KylinConfig.java @@ -189,7 +189,10 @@ private static UriType decideUriType(String metaUri) { try { File file = new File(metaUri); - if (file.exists()) { + if (file.exists() || metaUri.contains("/")) { + if (file.exists() == false) { + file.mkdirs(); + } if (file.isDirectory()) { return UriType.LOCAL_FOLDER; } else if (file.isFile()) { @@ -198,6 +201,8 @@ private static UriType decideUriType(String metaUri) { } else { throw new IllegalStateException("Metadata uri : " + metaUri + " is a local file but not kylin.properties"); } + } else { + throw new IllegalStateException("Metadata uri : " + metaUri + " looks like a file but it's neither a file nor a directory"); } } else { if (RestClient.matchFullRestPattern(metaUri)) @@ -209,8 +214,6 @@ private static UriType decideUriType(String metaUri) { logger.info(e.getLocalizedMessage()); throw new IllegalStateException("Metadata uri : " + metaUri + " is not recognized"); } - - return null; } public static KylinConfig createInstanceFromUri(String uri) { diff --git a/common/src/main/java/com/kylinolap/common/util/CliCommandExecutor.java b/common/src/main/java/com/kylinolap/common/util/CliCommandExecutor.java index 3f4ab7b72..0b2318cdb 100644 --- a/common/src/main/java/com/kylinolap/common/util/CliCommandExecutor.java +++ b/common/src/main/java/com/kylinolap/common/util/CliCommandExecutor.java @@ -48,7 +48,7 @@ public void setRunAtLocal() { this.remoteUser = null; this.remotePwd = null; } - + public void copyFile(String localFile, String destDir) throws IOException { if (remoteHost == null) copyNative(localFile, destDir); @@ -81,7 +81,9 @@ public String execute(String command) throws IOException { r = runRemoteCommand(command); if (r.getFirst() != 0) - throw new IOException("OS command error exit with " + r.getFirst() + " -- " + command + "\n" + r.getSecond()); + throw new IOException("OS command error exit with " + r.getFirst() // + + (remoteHost == null ? "" : " (remoteHost:" + remoteHost + ")") // + + " -- " + command + "\n" + r.getSecond()); return r.getSecond(); } diff --git a/common/src/test/java/com/kylinolap/common/util/MailServiceTest.java b/common/src/test/java/com/kylinolap/common/util/MailServiceTest.java index 044d13442..f6368d6f6 100644 --- a/common/src/test/java/com/kylinolap/common/util/MailServiceTest.java +++ b/common/src/test/java/com/kylinolap/common/util/MailServiceTest.java @@ -48,7 +48,7 @@ public void testSendEmail() throws IOException { private boolean sendTestEmail(MailService mailservice) { List receivers = new ArrayList(1); - receivers.add("shaoshi@ebay.com"); + receivers.add("foobar@foobar.com"); try { return mailservice.sendMail(receivers, "A test email from Kylin", "Hello!"); } catch (IOException e) { diff --git a/cube/src/main/java/com/kylinolap/cube/common/BytesSplitter.java b/cube/src/main/java/com/kylinolap/cube/common/BytesSplitter.java index fec0db191..0e18fa2d4 100644 --- a/cube/src/main/java/com/kylinolap/cube/common/BytesSplitter.java +++ b/cube/src/main/java/com/kylinolap/cube/common/BytesSplitter.java @@ -77,25 +77,6 @@ public int split(byte[] bytes, int byteLen, byte delimiter) { return bufferSize; } - public static List splitToString(byte[] bytes, int offset, byte delimiter) { - List splitStrings = new ArrayList(); - int splitOffset = 0; - int splitLength = 0; - for (int i = offset; i < bytes.length; i++) { - if (bytes[i] == delimiter) { - String str = Bytes.toString(bytes, splitOffset, splitLength); - splitStrings.add(str); - splitOffset = i + 1; - splitLength = 0; - } else { - splitLength++; - } - } - String str = Bytes.toString(bytes, splitOffset, splitLength); - splitStrings.add(str); - return splitStrings; - } - public byte inferByteRowDelimiter(byte[] bytes, int byteLen, int expectedSplits) throws IOException { if (expectedSplits > this.splitBuffers.length) @@ -138,4 +119,36 @@ public int detectDelim(Text value, int expectedParts) { throw new RuntimeException("Cannot detect delimeter from first line -- " + value.toString() + " -- expect " + expectedParts + " columns"); } + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("["); + for (int i = 0; i < bufferSize; i++) { + if (i > 0) + buf.append(", "); + + buf.append(Bytes.toString(splitBuffers[i].value, 0, splitBuffers[i].length)); + } + return buf.toString(); + } + + public static List splitToString(byte[] bytes, int offset, byte delimiter) { + List splitStrings = new ArrayList(); + int splitOffset = 0; + int splitLength = 0; + for (int i = offset; i < bytes.length; i++) { + if (bytes[i] == delimiter) { + String str = Bytes.toString(bytes, splitOffset, splitLength); + splitStrings.add(str); + splitOffset = i + 1; + splitLength = 0; + } else { + splitLength++; + } + } + String str = Bytes.toString(bytes, splitOffset, splitLength); + splitStrings.add(str); + return splitStrings; + } + } diff --git a/deploy/healthmon.sh b/deploy/healthmon.sh new file mode 100644 index 000000000..439eef400 --- /dev/null +++ b/deploy/healthmon.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +ALERT="your@email.com" + +OUTPUT=$( + curl --max-time 20 -# \ + --data '{"sql":"select count(*) from test_kylin_fact","offset":0,"limit":50000,"acceptPartial":true,"project":"default"}' \ + -H "Authorization:Basic QURNSU46S1lMSU4=" \ + -H "Content-Type:application/json;charset=UTF-8" \ + http://localhost:7070/kylin/api/query \ +) + +# ---------------------------------------------------------------------------- + +date + +if [[ $OUTPUT == *"results"* ]]; then + echo "Good." +else + echo "Bad." + TS_FILE=/tmp/kylin_healthmon_ts + LAST_TS=`stat -c%Y $TS_FILE 2>/dev/null` + CURR_TS=`date +%s` + echo last: $LAST_TS + echo curr: $CURR_TS + if (( ${LAST_TS:-"0"} < $CURR_TS - 3600 )); then + echo "Sending mail..." + echo "Kylin Prod health check failed as of $(date)." | mail -s "KYLIN PROD DOWN" $ALERT + if [ "$?" == "0" ]; then + touch $TS_FILE + fi + fi +fi diff --git a/deploy/install.sh b/deploy/install.sh index a8c7108af..6792fe0d4 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash cd ~ diff --git a/dictionary/src/main/java/com/kylinolap/dict/DictionaryGenerator.java b/dictionary/src/main/java/com/kylinolap/dict/DictionaryGenerator.java index 16ae61cd9..f4740359b 100644 --- a/dictionary/src/main/java/com/kylinolap/dict/DictionaryGenerator.java +++ b/dictionary/src/main/java/com/kylinolap/dict/DictionaryGenerator.java @@ -40,6 +40,8 @@ @SuppressWarnings({ "rawtypes", "unchecked" }) public class DictionaryGenerator { + private static final int DICT_MAX_CARDINALITY = 2000000; // 2 million + private static final Logger logger = LoggerFactory.getLogger(DictionaryGenerator.class); private static final String[] DATE_PATTERNS = new String[] { "yyyy-MM-dd" }; @@ -71,8 +73,8 @@ else if (dataType.isNumberFamily()) logger.info("Dictionary value samples: " + buf.toString()); logger.info("Dictionary cardinality " + info.getCardinality()); - if (values.size() > 1000000) - throw new IllegalArgumentException("Too high cardinality is not suitable for dictionary! Are the values stable enough for incremental load??"); + if (values.size() > DICT_MAX_CARDINALITY) + throw new IllegalArgumentException("Too high cardinality is not suitable for dictionary -- " + info.getSourceTable() + "." + info.getSourceColumn() + " cardinality: " + values.size()); return dict; } @@ -113,7 +115,7 @@ public static Dictionary buildDictionary(DictionaryInfo info, ReadableTable i private static Dictionary buildDateStrDict(List values, int baseId, int nSamples, ArrayList samples) { final int BAD_THRESHOLD = 2; String matchPattern = null; - + for (String ptn : DATE_PATTERNS) { matchPattern = ptn; // be optimistic int badCount = 0; diff --git a/dictionary/src/main/java/com/kylinolap/dict/lookup/SnapshotManager.java b/dictionary/src/main/java/com/kylinolap/dict/lookup/SnapshotManager.java index a47e614e2..ac52e268a 100644 --- a/dictionary/src/main/java/com/kylinolap/dict/lookup/SnapshotManager.java +++ b/dictionary/src/main/java/com/kylinolap/dict/lookup/SnapshotManager.java @@ -123,7 +123,7 @@ private String checkDupByInfo(SnapshotTable snapshot) throws IOException { // direct // load from // store - if (sig.equals(existingTable.getSignature())) + if (existingTable != null && sig.equals(existingTable.getSignature())) return existing; } @@ -153,6 +153,7 @@ private void save(SnapshotTable snapshot) throws IOException { } private SnapshotTable load(String resourcePath, boolean loadData) throws IOException { + logger.info("Loading snapshotTable from " + resourcePath + ", with loadData: " + loadData); ResourceStore store = MetadataManager.getInstance(this.config).getStore(); SnapshotTable table = store.getResource(resourcePath, SnapshotTable.class, loadData ? SnapshotTableSerializer.FULL_SERIALIZER : SnapshotTableSerializer.INFO_SERIALIZER); diff --git a/docs/Design Cube in Kylin.pdf b/docs/Design Cube in Kylin.pdf new file mode 100644 index 000000000..922de028f Binary files /dev/null and b/docs/Design Cube in Kylin.pdf differ diff --git a/docs/Installation/Frequently Asked Questions on Installation.md b/docs/Installation/Frequently Asked Questions on Installation.md new file mode 100644 index 000000000..c16040309 --- /dev/null +++ b/docs/Installation/Frequently Asked Questions on Installation.md @@ -0,0 +1,26 @@ +Frequently Asked Questions on Installation +--- +* Some NPM error causes ERROR exit (中国大陆地区用户请特别注意此问题)? +> Check out https://github.com/KylinOLAP/Kylin/issues/35 + +* Can't get master address from ZooKeeper" when installing Kylin on Hortonworks Sandbox +> Check out https://github.com/KylinOLAP/Kylin/issues/9. + +* Install scripted finished in my virtual machine, but cannot visit via http://localhost:9080 +> Check out https://github.com/KylinOLAP/Kylin/issues/12. + +* Map Reduce Job information can't display on sandbox deployment +> Check out https://github.com/KylinOLAP/Kylin/issues/40 + +* Install Kylin on CDH 5.2 or Hadoop 2.5.x +> Check out discussion: https://groups.google.com/forum/?utm_medium=email&utm_source=footer#!msg/kylin-olap/X0GZfsX1jLc/nzs6xAhNpLkJ +> +``` +I was able to deploy Kylin with following option in POM. +2.5.0 +2.5.0 +0.98.6-hadoop2 +3.4.5 +0.13.1 +My Cluster is running on Cloudera Distribution CDH 5.2.0. +``` diff --git a/docs/Installation/Frequently Asked Questions on Installation.pdf b/docs/Installation/Frequently Asked Questions on Installation.pdf new file mode 100644 index 000000000..ff340abe7 Binary files /dev/null and b/docs/Installation/Frequently Asked Questions on Installation.pdf differ diff --git a/docs/Installation/Off Hadoop CLI Installation (Dev Env Setup).md b/docs/Installation/Off Hadoop CLI Installation (Dev Env Setup).md new file mode 100644 index 000000000..42cf28590 --- /dev/null +++ b/docs/Installation/Off Hadoop CLI Installation (Dev Env Setup).md @@ -0,0 +1,76 @@ +Off Hadoop CLI Installation (Dev Env Setup) +=== +Off-Hadoop-CLI installation is usually for **development use**. + +Developers want to run kylin test cases or applications at their development machine. The scenario is depicted at https://github.com/KylinOLAP/Kylin#off-hadoop-cli-installation. + + +By following this tutorial, you will be able to build kylin test cubes by running a specific test case, and you can further run other test cases against the cubes having been built. + + +## Environment on the Hadoop CLI + +Off-Hadoop-CLI installation requires you having a hadoop CLI machine (or a hadoop sandbox) as well as your local develop machine. To make things easier we strongly recommend you starting with running Kylin on a hadoop sandbox, like . In the following tutorial we'll go with **Hortonworks Sandbox 2.1**. + +### Start Hadoop + +In Hortonworks sandbox, ambari helps to launch hadoop: + + ambari-agent start + ambari-server start + +With both command successfully run you can go to ambari home page at (user:admin,password:admin) to check everything's status. By default ambari disables Hbase, you'll need manually start the `Hbase` service. + +For other hadoop distribution, basically start the hadoop cluster, make sure HDFS, YARN, Hive, HBase are running. + + +## Environment on the dev machine + +### Install maven + +The latest maven can be found at , we create a symbolic so that `mvn` can be run anywhere. + + cd ~ + wget http://apache.proserve.nl/maven/maven-3/3.2.3/binaries/apache-maven-3.2.3-bin.tar.gz + tar -xzvf apache-maven-3.2.3-bin.tar.gz + ln -s /root/apache-maven-3.2.3/bin/mvn /usr/bin/mvn + +### Compile + +First clone the Kylin project to your local: + + git clone https://github.com/KylinOLAP/Kylin.git + +Install Kylin artifacts to the maven repo + + mvn clean install -DskipTests + +### Modify local configuration + +Local configuration must be modified to point to your hadoop sandbox (or CLI) machine. If you are using a Hortonworks sandbox, this section may be skipped. + +* In **examples/test_case_data/sandbox/kylin.properties** + * Find `sandbox` and replace with your hadoop hosts + * Find `kylin.job.remote.cli.username` and `kylin.job.remote.cli.password`, fill in the user name and password used to login hadoop cluster for hadoop command execution + +* In **examples/test_case_data/sandbox** + * For each configuration xml file, find all occurrence of `sandbox` and replace with your hadoop hosts + +An alternative to the host replacement is updating your `hosts` file to resolve `sandbox` and `sandbox.hortonworks.com` to the IP of your sandbox machine. + +### Run unit tests + +Run a end-to-end cube building test + + mvn test -Dtest=com.kylinolap.job.BuildCubeWithEngineTest -DfailIfNoTests=false + +Run other tests, the end-to-end cube building test is exclueded + + mvn test + +### Launch Kylin Web Server + +In your Eclipse IDE, launch `com.kylinolap.rest.DebugTomcat` with specifying VM arguments "-Dspring.profiles.active=sandbox". (By default Kylin server will listen on 7070 port; If you want to use another port, please specify it as a parameter when run `DebugTomcat) + +Check Kylin Web available at http://localhost:7070 (user:ADMIN,password:KYLIN) + diff --git a/docs/Installation/Off Hadoop CLI Installation (Dev Env Setup).pdf b/docs/Installation/Off Hadoop CLI Installation (Dev Env Setup).pdf new file mode 100644 index 000000000..1cdf8da3e Binary files /dev/null and b/docs/Installation/Off Hadoop CLI Installation (Dev Env Setup).pdf differ diff --git a/docs/Installation/On Hadoop CLI installation.md b/docs/Installation/On Hadoop CLI installation.md new file mode 100644 index 000000000..a71872242 --- /dev/null +++ b/docs/Installation/On Hadoop CLI installation.md @@ -0,0 +1,102 @@ +On Hadoop CLI installation +=== +On-Hadoop-CLI installation is for demo use, or for those who want to host their own web site to provide Kylin service. The scenario is depicted at https://github.com/KylinOLAP/Kylin#on-hadoop-cli-installation. + +Except for some prerequisite software installations, the core of Kylin installation is accomplished by running a single script. After running the script, you will be able to build sample cube and query the tables behind the cubes via a unified web interface. + +## Environment + +Running On-Hadoop-CLI installation requires you having access to a hadoop CLI, where you have full permissions to hdfs, hive, hbase and map-reduce. To make things easier we strongly recommend you starting with running Kylin on a hadoop sandbox, like . In the following tutorial we'll go with **Hortonworks Sandbox 2.1** and **Cloudera QuickStart VM 5.1**. + +To avoid permission issue, we suggest you using `root` account. The password for **Hortonworks Sandbox 2.1** is `hadoop` , for **Cloudera QuickStart VM 5.1** is `cloudera`. + +We also suggest you using bridged mode instead of NAT mode in your virtual box settings. Bridged mode will assign your sandbox an independent IP so that you can avoid issues like https://github.com/KylinOLAP/Kylin/issues/12 + +### Start Hadoop + +For Hortonworks, ambari helps to launch hadoop: + + ambari-agent start + ambari-server start + +With both command successfully run you can go to ambari homepage at (user:admin,password:admin) to check everything's status. **By default hortonworks ambari disables Hbase, you'll need manually start the `Hbase` service at ambari homepage.** + +![start hbase in ambari](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/installation/starthbase.png) + +For Cloudera, you can skip this step since they're by default activated. + +### Install tomcat + +The latest tomcat can be found at , we need the variable `CATALINA_HOME` exported. + + cd ~ + wget http://apache.cs.uu.nl/dist/tomcat/tomcat-7/v7.0.57/bin/apache-tomcat-7.0.57.tar.gz + tar -xzvf apache-tomcat-7.0.57.tar.gz + export CATALINA_HOME=~/apache-tomcat-7.0.57 + + +### Install maven: + +For Cloudera, you can skip this step since maven is by default installed. + +The latest maven can be found at , we create a symbolic so that `mvn` can be run anywhere. + + cd ~ + wget http://apache.proserve.nl/maven/maven-3/3.2.3/binaries/apache-maven-3.2.3-bin.tar.gz + tar -xzvf apache-maven-3.2.3-bin.tar.gz + ln -s ~/apache-maven-3.2.3/bin/mvn /usr/bin/mvn + + +### Install npm + +Npm comes with latest versions of nodejs, just donwload nodejs from , `npm` is located at the `/bin` folder of `nodejs`, we append the folder to `PATH` so that `npm` can be run anywhere. + + cd ~ + wget http://nodejs.org/dist/v0.10.32/node-v0.10.32-linux-x64.tar.gz + tar -xzvf node-v0.10.32-linux-x64.tar.gz + export PATH=~/node-v0.10.32-linux-x64/bin:$PATH + + +## Build cubes & Query tables + +First clone the Kylin project to your local: + + git clone https://github.com/KylinOLAP/Kylin.git + +Go into the folder and run deploy script: + + cd Kylin/ + ./deploy.sh + +If you meet any problems, please check [FAQ](https://github.com/KylinOLAP/Kylin/wiki/Frequently-Asked-Questions-on-Installation) first. +This script will help to: + +1. Check your environment +2. Build Kylin artifacts with Maven +3. Generate some sample tables +3. Create empty cubes for these tables +4. Lauch a one-stop website to build cubes & query tables + +After successfully running the script, please navigate to to build your cube and query it. The username/password is ADMIN/KYLIN + +### Build Cube in Cubes Tab + +![build cube](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/installation/cube.png) +### Check Building Progress in Job Tab +![check job status](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/installation/job.png) +### Query Tables in Query Tab +![query tables](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/installation/query.png) + +## What if I closed my VM? + +If you shut down the VM and restarted it, Kylin will not automatically start. Depending on whether you succeed in running the deploy.sh, you should: + +### If running deploy.sh failed last time +1. Kill it if any tomcat instance exist +2. Start over again + +### If running deploy.sh succeeded last time +1. Kill it if any tomcat instance exist +2. Make sure Hbase is running +3. run `export CATALINA_HOME=~/apache-tomcat-7.0.56` +4. run `sudo -i "${CATALINA_HOME}/bin/startup.sh"` \ No newline at end of file diff --git a/docs/Installation/On Hadoop CLI installation.pdf b/docs/Installation/On Hadoop CLI installation.pdf new file mode 100644 index 000000000..636555a54 Binary files /dev/null and b/docs/Installation/On Hadoop CLI installation.pdf differ diff --git a/docs/Installation/On Hadoop Kylin installation using Docker.md b/docs/Installation/On Hadoop Kylin installation using Docker.md new file mode 100644 index 000000000..81b2d7f49 --- /dev/null +++ b/docs/Installation/On Hadoop Kylin installation using Docker.md @@ -0,0 +1,40 @@ +On Hadoop Kylin installation using Docker +=== +With help of SequenceIQ, we have put together a fully automated method of creating a Kylin cluster (along with Hadoop, HBase and Hive). The only thing you will need to do is to pull the container from the official Docker repository by using the commands listed below: + +### Pre-Requisite + +1. Docker (If you don't have Docker installed, follow this [link](https://docs.docker.com/installation/#installation)) +2. Minimum RAM - 4Gb (We'll be running Kylin, Hadoop, HBase & Hive) + +### Installation +``` +docker pull sequenceiq/kylin +``` + +Once the container is pulled you are ready to start playing with Kylin. Get the following helper functions from our Kylin GitHub [repository](https://github.com/sequenceiq/docker-kylin/blob/master/ambari-functions) - _(make sure you source it)._ + +``` + $ wget https://raw.githubusercontent.com/sequenceiq/docker-kylin/master/ambari-functions + $ source ambari-functions +``` +``` + $ kylin-deploy-cluster 3 +``` + +You can specify the number of nodes you'd like to have in your cluster (3 in this case). Once we installed all the necessary Hadoop +services we'll build Kylin on top of it and then you can reach the UI on: + +``` +#Ambari Dashboard +http://:8080 +``` + +Use `admin/admin` to login. Make sure HBase is running. + +``` +#Kylin Dashboard +http://:7070 +``` +The default credentials to login are: `ADMIN:KYLIN`. The cluster is pre-populated with sample data and is ready to build cubes as shown [here](https://github.com/KylinOLAP/Kylin/wiki/Kylin-Cube-Creation-Tutorial). + \ No newline at end of file diff --git a/docs/Installation/On Hadoop Kylin installation using Docker.pdf b/docs/Installation/On Hadoop Kylin installation using Docker.pdf new file mode 100644 index 000000000..a30fe158f Binary files /dev/null and b/docs/Installation/On Hadoop Kylin installation using Docker.pdf differ diff --git a/docs/Tutorial/Kylin Cube Build and Job Monitoring Tutorial.md b/docs/Tutorial/Kylin Cube Build and Job Monitoring Tutorial.md new file mode 100644 index 000000000..827cd94ac --- /dev/null +++ b/docs/Tutorial/Kylin Cube Build and Job Monitoring Tutorial.md @@ -0,0 +1,60 @@ +Kylin Cube Build and Job Monitoring Tutorial +=== + +### Cube Build +First of all, make sure that you have authority of the cube you want to build. + +1. In `Cubes` page, click the `Action` drop down button in the right of a cube column and select operation `Build`. + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/cube_build_job_monitor/1%20action-build.png) + +2. There is a pop-up window after the selection. + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/cube_build_job_monitor/2%20pop-up.png) + +3. Click `END DATE` input box to choose end date of this incremental cube build. + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/cube_build_job_monitor/3%20end-date.png) + +4. Click `Submit` to send request. + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/cube_build_job_monitor/4%20submit.png) + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/cube_build_job_monitor/4.1%20success.png) + + After submit the request successfully, you will see the job just be created in the `Jobs` page. + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/cube_build_job_monitor/5%20jobs-page.png) + +5. To discard this job, just click the `Discard` button. + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/cube_build_job_monitor/6%20discard.png) + +### Job Monitoring +In the `Jobs` page, click the job detail button to see detail information show in the right side. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/cube_build_job_monitor/7%20job-steps.png) + +The detail information of a job provides a step-by-step record to trace a job. You can hover a step status icon to see the basic status and information. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/cube_build_job_monitor/8%20hover-step.png) + +Click the icon button show in each step to see the details: `Parameters`, `Log`, `MRJob`, `EagleMonitoring`. + +* Parameters + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/cube_build_job_monitor/9%20parameters.png) + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/cube_build_job_monitor/9%20parameters-d.png) + +* Log + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/cube_build_job_monitor/9%20log.png) + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/cube_build_job_monitor/9%20log-d.png) + +* MRJob(MapReduce Job) + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/cube_build_job_monitor/9%20mrjob.png) + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/cube_build_job_monitor/9%20mrjob-d.png) \ No newline at end of file diff --git a/docs/Tutorial/Kylin Cube Build and Job Monitoring Tutorial.pdf b/docs/Tutorial/Kylin Cube Build and Job Monitoring Tutorial.pdf new file mode 100644 index 000000000..b4253d476 Binary files /dev/null and b/docs/Tutorial/Kylin Cube Build and Job Monitoring Tutorial.pdf differ diff --git a/docs/Tutorial/Kylin Cube Creation Tutorial.md b/docs/Tutorial/Kylin Cube Creation Tutorial.md new file mode 100644 index 000000000..cc8507505 --- /dev/null +++ b/docs/Tutorial/Kylin Cube Creation Tutorial.md @@ -0,0 +1,122 @@ +Kylin Cube Creation Tutorial +=== + +### I. Create a Project +1. Go to `Query` page in top menu bar, then click `Manage Projects`. + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/1%20manage-prject.png) + +2. Click the `+ Project` button to add a new project. + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/2%20%2Bproject.png) + +3. Fulfill the following form and click `submit` button to send a request. + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/3%20new-project.png) + +4. After success, there will be a notification show in the bottom. + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/3.1%20pj-created.png) + +### II. Sync up a Table +1. Click `Tables` in top bar and then click the `+ Sync` button to load hive table metadata. + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/4%20%2Btable.png) + +2. Enter the table names and click `Sync` to send a request. + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/5%20hive-table.png) + +### III. Create a Cube +To start with, click `Cubes` in top bar.Then click `+Cube` button to enter the cube designer page. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/6%20%2Bcube.png) + +**Step 1. Cube Info** + +Fill up the basic information of the cube. Click `Next` to enter the next step. + +You can use letters, numbers and '_' to name your cube (Notice that space in name is not allowed). + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/7%20cube-info.png) + +**Step 2. Dimensions** + +1. Set up the fact table. + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/8%20dim-factable.png) + +2. Click `+Dimension` to add a new dimension. + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/8%20dim-%2Bdim.png) + +3. There are different types of dimensions that might be added to a cube. Here we list some of them for your reference. + + * Dimensions from fact table. + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/8%20dim-typeA.png) + + * Dimensions from look up table. + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/8%20dim-typeB-1.png) + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/8%20dim-typeB-2.png) + + * Dimensions from look up table with hierarchy. + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/8%20dim-typeC.png) + + * Dimensions from look up table with derived dimensions. + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/8%20dim-typeD.png) + +4. User can edit the dimension after saving it. + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/8%20dim-edit.png) + +**Step 3. Measures** + +1. Click the `+Measure` to add a new measure. + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/9%20meas-%2Bmeas.png) + +2. There are 5 different types of measure according to its expression: `SUM`, `MAX`, `MIN`, `COUNT` and `COUNT_DISTINCT`. Please be carefully to choose the return type, which is related to the error rate of the `COUNT(DISTINCT)`. + * SUM + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/9%20meas-sum.png) + + * MIN + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/9%20meas-min.png) + + * MAX + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/9%20meas-max.png) + + * COUNT + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/9%20meas-count.png) + + * DISTINCT_COUNT + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/9%20meas-distinct.png) + +**Step 4. Filter** + +This step is optional. You can add some condition filter in `SQL` format. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/10%20filter.png) + +**Step 5. Refresh Setting** + +This step is designed for incremental cube build. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/11%20refresh-setting1.png) + +Choose partition type, partition column and start date. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/11%20refresh-setting2.png) + +**Step 6. Advanced Setting** + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/12%20advanced.png) + +**Step 7. Overview & Save** + +You can overview your cube and go back to previous step to modify it. Click the `Save` button to complete the cube creation. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/13%20overview.png) \ No newline at end of file diff --git a/docs/Tutorial/Kylin Cube Creation Tutorial.pdf b/docs/Tutorial/Kylin Cube Creation Tutorial.pdf new file mode 100644 index 000000000..cd6e934c3 Binary files /dev/null and b/docs/Tutorial/Kylin Cube Creation Tutorial.pdf differ diff --git a/docs/Tutorial/Kylin Cube Permission Grant Tutorial.md b/docs/Tutorial/Kylin Cube Permission Grant Tutorial.md new file mode 100644 index 000000000..8775bd6a8 --- /dev/null +++ b/docs/Tutorial/Kylin Cube Permission Grant Tutorial.md @@ -0,0 +1,27 @@ +Kylin Cube Permission Grant Tutorial +=== + +In `Cubes` page, double click the cube row to see the detail information. Here we focus on the `Access` tab. +Click the `+Grant` button to grant permission. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/14%20+grant.png) + +There are four different kinds of permissions for a cube. Move your mouse over the `?` icon to see detail information. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/15%20grantInfo.png) + +There are also two types of user that a permission can be granted: `User` and `Role`. `Role` means a group of users who have the same role. + +### 1. Grant User Permission +* Select `User` type, enter the username of the user you want to grant and select the related permission. + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/16%20grant-user.png) + +* Then click the `Grant` button to send a request. After the success of this operation, you will see a new table entry show in the table. You can select various permission of access to change the permission of a user. To delete a user with permission, just click the `Revoke` button. + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tutorial/16%20user-update.png) + +### 2. Grant Role Permission +* Select `Role` type, choose a group of users that you want to grant by click the drop down button and select a permission. + +* Then click the `Grant` button to send a request. After the success of this operation, you will see a new table entry show in the table. You can select various permission of access to change the permission of a group. To delete a group with permission, just click the `Revoke` button. diff --git a/docs/Tutorial/Kylin Cube Permission Grant Tutorial.pdf b/docs/Tutorial/Kylin Cube Permission Grant Tutorial.pdf new file mode 100644 index 000000000..adfb6d7cc Binary files /dev/null and b/docs/Tutorial/Kylin Cube Permission Grant Tutorial.pdf differ diff --git a/docs/Tutorial/Kylin ODBC Driver Tutorial.md b/docs/Tutorial/Kylin ODBC Driver Tutorial.md new file mode 100644 index 000000000..43439fc16 --- /dev/null +++ b/docs/Tutorial/Kylin ODBC Driver Tutorial.md @@ -0,0 +1,30 @@ +Kylin ODBC Driver Tutorial +=== + +> We provide Kylin ODBC driver to enable data access from ODBC-compatible client applications. + +> Both 32-bit version or 64-bit version driver are available. + +> Tested Operation System: Windows 7, Windows Server 2008 R2 + +> Tested Application: Tableau 8.0.4 and Tableau 8.1.3 + +## Prerequisites +1. Microsoft Visual C++ 2012 Redistributable + + * For 32 bit Windows or 32 bit Tableau Desktop: Download: [32bit version](http://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x86.exe) + * For 64 bit Windows or 64 bit Tableau Desktop: Download: [64bit version](http://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe) + +2. ODBC driver internally gets results from a REST server, make sure you have access to one + +## Installation +1. Uninstall existing Kylin ODBC first, if you already installled it before +2. Download the attached driver installer at [KylinOLAP/odbc-driver/exe](https://github.com/KylinOLAP/odbc-driver/tree/master/exe), run it. + + * For 32 bit Tableau Desktop: Please install KylinODBCDriver (x86).exe + * For 64 bit Tableau Desktop: Please install KylinODBCDriver (x64).exe + +3. Both drivers already be installed on Tableau Server, you properly should be able to publish to there without issues + +## Bug Report +Open github issue here \ No newline at end of file diff --git a/docs/Tutorial/Kylin ODBC Driver Tutorial.pdf b/docs/Tutorial/Kylin ODBC Driver Tutorial.pdf new file mode 100644 index 000000000..e2d4986f4 Binary files /dev/null and b/docs/Tutorial/Kylin ODBC Driver Tutorial.pdf differ diff --git a/docs/Tutorial/Kylin Web Tutorial.md b/docs/Tutorial/Kylin Web Tutorial.md new file mode 100644 index 000000000..53283705d --- /dev/null +++ b/docs/Tutorial/Kylin Web Tutorial.md @@ -0,0 +1,133 @@ +Kylin Web Tutorial +=== + +> **Supported Browsers** + +> Windows: Google Chrome, FireFox + +> Mac: Google Chrome, FireFox, Safari + +## 1. Access & Login +Host to access: http://your_sandbox_ip:9080 +Login with username/password: ADMIN/KYLIN + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/web%20tutorial/1%20login.png) + +## 2. Available Hive Tables in Kylin +Although Kylin will using SQL as query interface and leverage Hive metadata, kylin will not enable user to query all hive tables since it's a pre-build OLAP (MOLAP) system so far. To enable Table in Kylin, it will be easy to using "Sync" function to sync up tables from Hive. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/web%20tutorial/2%20tables.png) + +## 3. Kylin OLAP Cube +Kylin's OLAP Cubes are pre-calculation datasets from Star Schema Hive tables, Here's the web management interface for user to explorer, manage all cubes.Go to `Cubes` Menu, it will list all cubes available in system: + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/web%20tutorial/3%20cubes.png) + +To explore more detail about the Cube + +* Form View: + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/web%20tutorial/4%20form-view.png) + +* SQL View (Underline Hive Query to generate the cube): + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/web%20tutorial/5%20sql-view.png) + +* Visualization (Showing the Star Schema behind of this cube): + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/web%20tutorial/6%20visualization.png) + +* Access (Grant user/role privileges, Grant operation only open to Admin in beta): + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/web%20tutorial/7%20access.png) + +## 4. Write and Execute SQL on web +Kylin's web offer a simple query tool for user to run SQL to explorer existing cube, verify result and explorer the result set using #5's Pivot analysis and visualization + +> **Query Limit** + +> 1. Only SELECT query be supported + +> 2. To avoid huge network traffic from server to client, the underline scan range's threshold be set to 1,000,000 in beta. + +> 3. SQL can't found data from cube will not redirect to Hive in beta + +Go to "Query" menu: + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/web%20tutorial/8%20query.png) + +* Source Tables: + + Browser current available Tables (same structure and metadata as Hive): + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/web%20tutorial/9%20query-table.png) + +* New Query: + + You can write and execute your query and explorer the result. One query for you experience: + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/web%20tutorial/10%20query-result.png) + +* Saved Query: + + Associate with user account, you can get saved query from different browsers even machines. + Click "Save" in Result area, it will popup for name and description to save current query: + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/web%20tutorial/11%20save-query.png) + + Click "Saved Queries" to browser all your saved queries, you could direct resubmit it to run or remove it: + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/web%20tutorial/11%20save-query-2.png) + +* Query History: + + Only keep the current user's query history in current bowser, it will require cookie enabled and will lost if you clean up bowser's cache.Click "Query History" tab, you could directly resubmit any of them to execute again. + +## 5. Pivot Analysis and Visualization +There's one simple pivot and visualization analysis tool in Kylin's web for user to explorer their query result: + +* General Information: + + When the query execute success, it will present a success indictor and also a cube's name which be hit. + Also it will present how long this query be executed in backend engine (not cover network traffic from Kylin server to browser): + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/web%20tutorial/12%20general.png) + +* Query Result: + + It's easy to order on one column. + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/web%20tutorial/13%20results.png) + +* Export to CSV File + + Click "Export" button to save current result as CSV file. + +* Pivot Table: + + Drag and Drop one or more columns into the header, the result will grouping by such column's value: + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/web%20tutorial/14%20drag.png) + +* Visualization: + + Also, the result set will be easy to show with different charts in "Visualization": + + note: line chart only available when there's at least one dimension with real "Date" data type of column from Hive Table. + + * Bar Chart: + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/web%20tutorial/15%20bar-chart.png) + + * Pie Chart: + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/web%20tutorial/16%20pie-chart.png) + + * Line Chart + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/web%20tutorial/17%20line-chart.png) + +## 6. Cube Build Job Monitoring +Monitor and manage cube build process, diagnostic into the detail and even link to Hadoop's job information directly: + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/cube_build_job_monitor/7%20job-steps.png) \ No newline at end of file diff --git a/docs/Tutorial/Kylin Web Tutorial.pdf b/docs/Tutorial/Kylin Web Tutorial.pdf new file mode 100644 index 000000000..6f27a4be9 Binary files /dev/null and b/docs/Tutorial/Kylin Web Tutorial.pdf differ diff --git a/docs/Tutorial/Kylin and Tableau Tutorial.md b/docs/Tutorial/Kylin and Tableau Tutorial.md new file mode 100644 index 000000000..8630dab94 --- /dev/null +++ b/docs/Tutorial/Kylin and Tableau Tutorial.md @@ -0,0 +1,106 @@ +Kylin and Tableau Tutorial +=== + +> There are some limitations of Kylin ODBC driver with Tableau, please read carefully this instruction before you try it. +> * Only support "managed" analysis path, Kylin engine will raise exception for un-excepted dimension or metric +> * Please always select Fact Table first, then add lookup tables with correct join condition (defined join type in cube) +> * Do not try to join between fact tables or lookup tables; +> * You can try to use high cardinality dimensions like seller id as Tableau Filter, but the engine will only return limited seller id in Tableau's filter now. + +> More detail information or any issue, please contact Kylin Team: `kylinolap@gmail.com` + +### Step 1. Install ODBC Driver +Refer to wiki page [Kylin ODBC Driver Tutorial](https://github.com/KylinOLAP/Kylin/wiki/Kylin-ODBC-Driver-Tutorial). + +### Step 2. Connect to Kylin Server +> We recommended to use Connect Using Driver instead of Using DSN since Tableau team will not manage your DSN on Tableau Servers. + +Connect Using Driver: Select "Other Database(ODBC)" in the left panel and choose KylinODBCDriver in the pop-up window. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tableau_tutorial/1%20odbc.png) + +Enter your Sever location and credentials: server host, port, username and password. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tableau_tutorial/2%20serverhost.jpg) + +Click "Connect" to get the list of projects that you have permission to access. See details about permission in [Kylin Cube Permission Grant Tutorial](https://github.com/KylinOLAP/Kylin/wiki/Kylin-Cube-Permission-Grant-Tutorial). Then choose the project you want to connect in the drop down list. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tableau_tutorial/3%20project.jpg) + +Click "Done" to complete the connection. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tableau_tutorial/4%20done.jpg) + +### Step 3. Using Single Table or Multiple Tables +> Limitation +> * Must select FACT table first +> * Do not support select from lookup table only +> * The join condition must match within cube definition + +**Select Fact Table** + +Select `Multiple Tables`. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tableau_tutorial/5%20multipleTable.jpg) + +Then click `Add Table...` to add a fact table. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tableau_tutorial/6%20facttable.jpg) + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tableau_tutorial/6%20facttable2.jpg) + +**Select Look-up Table** + +Click `Add Table...` to add a look-up table. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tableau_tutorial/7%20lkptable.jpg) + +Set up the join clause carefully. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tableau_tutorial/8%20join.jpg) + +Keep add tables through click `Add Table...` until all the look-up tables have been added properly. Give the connection a name for use in Tableau. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tableau_tutorial/9%20connName.jpg) + +**Using Connect Live** + +There are three types of `Data Connection`. Choose the `Connect Live` option. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tableau_tutorial/10%20connectLive.jpg) + +Then you can enjoy analyzing with Tableau. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tableau_tutorial/11%20analysis.jpg) + +**Add additional look-up Tables** + +Click `Data` in the top menu bar, select `Edit Tables...` to update the look-up table information. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tableau_tutorial/12%20edit%20tables.jpg) + +### Step 4. Using Customized SQL +To use customized SQL resembles using Single Table/Multiple Tables, except that you just need to paste your SQL in `Custom SQL` tab and take the same instruction as above. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tableau_tutorial/19%20custom.jpg) + +### Step 5. Publish to Tableau Server +Suppose you have finished making a dashboard with Tableau, you can publish it to Tableau Server. +Click `Server` in the top menu bar, select `Publish Workbook...`. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tableau_tutorial/14%20publish.jpg) + +Then sign in your Tableau Server and prepare to publish. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tableau_tutorial/16%20prepare-publish.png) + +If you're Using Driver Connect instead of DSN connect, you'll need to additionally embed your password in. Click the `Authentication` button at left bottom and select `Embedded Password`. Click `Publish` and you will see the result. + +![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tableau_tutorial/17%20embedded-pwd.png) + +### Tips +* Hide Table name in Tableau + + * Tableau will display columns be grouped by source table name, but user may want to organize columns with different structure. Using "Group by Folder" in Tableau and Create Folders to group different columns. + + ![](https://raw.githubusercontent.com/KylinOLAP/kylinolap.github.io/master/docs/tableau_tutorial/18%20groupby-folder.jpg) \ No newline at end of file diff --git a/docs/Tutorial/Kylin and Tableau Tutorial.pdf b/docs/Tutorial/Kylin and Tableau Tutorial.pdf new file mode 100644 index 000000000..14edbe20b Binary files /dev/null and b/docs/Tutorial/Kylin and Tableau Tutorial.pdf differ diff --git a/job/src/main/java/com/kylinolap/job/JobInstance.java b/job/src/main/java/com/kylinolap/job/JobInstance.java index 0a7308aab..48ad867a2 100644 --- a/job/src/main/java/com/kylinolap/job/JobInstance.java +++ b/job/src/main/java/com/kylinolap/job/JobInstance.java @@ -473,7 +473,7 @@ public int compareTo(JobStep o) { @Override public int compareTo(JobInstance o) { - return (int) (o.lastModified - this.lastModified); + return o.lastModifiedthis.lastModified?1:0; } } diff --git a/job/src/main/java/com/kylinolap/job/cmd/ShellCmd.java b/job/src/main/java/com/kylinolap/job/cmd/ShellCmd.java index 6ecb85c63..c9187cba1 100644 --- a/job/src/main/java/com/kylinolap/job/cmd/ShellCmd.java +++ b/job/src/main/java/com/kylinolap/job/cmd/ShellCmd.java @@ -228,7 +228,7 @@ public static void main(String[] args) throws JobException { ShellCmdOutput output = new ShellCmdOutput(); ShellCmd shellCmd = new ShellCmd(args[0], output, args[1], args[2], args[3], false); shellCmd.execute(); - + System.out.println("============================================================================"); System.out.println(output.getExitCode()); System.out.println(output.getOutput()); diff --git a/job/src/main/java/com/kylinolap/job/constant/BatchConstants.java b/job/src/main/java/com/kylinolap/job/constant/BatchConstants.java index b1dab021d..95ec8666e 100644 --- a/job/src/main/java/com/kylinolap/job/constant/BatchConstants.java +++ b/job/src/main/java/com/kylinolap/job/constant/BatchConstants.java @@ -42,5 +42,5 @@ public interface BatchConstants { public static final String CUBE_CAPACITY = "cube.capacity"; public static final int COUNTER_MAX = 100000; - public static final int ERROR_RECORD_THRESHOLD = 10; + public static final int ERROR_RECORD_THRESHOLD = 100; } diff --git a/job/src/main/java/com/kylinolap/job/hadoop/cube/BaseCuboidMapper.java b/job/src/main/java/com/kylinolap/job/hadoop/cube/BaseCuboidMapper.java index 39b39187f..20f4baf33 100644 --- a/job/src/main/java/com/kylinolap/job/hadoop/cube/BaseCuboidMapper.java +++ b/job/src/main/java/com/kylinolap/job/hadoop/cube/BaseCuboidMapper.java @@ -203,9 +203,10 @@ public void map(KEYIN key, Text value, Context context) throws IOException, Inte logger.info("Handled " + counter + " records!"); } - bytesSplitter.split(value.getBytes(), value.getLength(), byteRowDelimiter); - try { + bytesSplitter.split(value.getBytes(), value.getLength(), byteRowDelimiter); + intermediateTableDesc.sanityCheck(bytesSplitter); + byte[] rowKey = buildKey(bytesSplitter.getSplitBuffers()); outputKey.set(rowKey, 0, rowKey.length); @@ -214,23 +215,13 @@ public void map(KEYIN key, Text value, Context context) throws IOException, Inte context.write(outputKey, outputValue); } catch (Exception ex) { - handleErrorRecord(bytesSplitter.getSplitBuffers(), ex); + handleErrorRecord(bytesSplitter, ex); } } - private void handleErrorRecord(SplittedBytes[] splitBuffers, Exception ex) throws IOException { + private void handleErrorRecord(BytesSplitter bytesSplitter, Exception ex) throws IOException { - StringBuilder buf = new StringBuilder(); - buf.append("Error record: ["); - for (int i = 0; i < splitBuffers.length; i++) { - if (i > 0) - buf.append(", "); - - buf.append(Bytes.toString(splitBuffers[i].value, 0, splitBuffers[i].length)); - } - buf.append("] -- "); - buf.append(ex.toString()); - System.err.println(buf.toString()); + System.err.println("Insane record: " + bytesSplitter); ex.printStackTrace(System.err); errorRecordCounter++; diff --git a/job/src/main/java/com/kylinolap/job/hadoop/cube/FactDistinctColumnsMapper.java b/job/src/main/java/com/kylinolap/job/hadoop/cube/FactDistinctColumnsMapper.java index 236658d0e..c95018ed1 100644 --- a/job/src/main/java/com/kylinolap/job/hadoop/cube/FactDistinctColumnsMapper.java +++ b/job/src/main/java/com/kylinolap/job/hadoop/cube/FactDistinctColumnsMapper.java @@ -55,6 +55,7 @@ public class FactDistinctColumnsMapper extends Mapper BatchConstants.ERROR_RECORD_THRESHOLD) { + if (ex instanceof IOException) + throw (IOException) ex; + else if (ex instanceof RuntimeException) + throw (RuntimeException) ex; + else + throw new RuntimeException("", ex); + } + } } diff --git a/job/src/main/java/com/kylinolap/job/hadoop/cube/RangeKeyDistributionReducer.java b/job/src/main/java/com/kylinolap/job/hadoop/cube/RangeKeyDistributionReducer.java index dafea36ca..da9d2e38f 100644 --- a/job/src/main/java/com/kylinolap/job/hadoop/cube/RangeKeyDistributionReducer.java +++ b/job/src/main/java/com/kylinolap/job/hadoop/cube/RangeKeyDistributionReducer.java @@ -32,9 +32,9 @@ */ public class RangeKeyDistributionReducer extends Reducer { - public static final long FIVE_GIGA_BYTES = 5L * 1024L * 1024L * 1024L; public static final long TEN_GIGA_BYTES = 10L * 1024L * 1024L * 1024L; public static final long TWENTY_GIGA_BYTES = 20L * 1024L * 1024L * 1024L; + public static final long HUNDRED_GIGA_BYTES = 100L * 1024L * 1024L * 1024L; private LongWritable outputValue = new LongWritable(0); @@ -49,13 +49,13 @@ protected void setup(Context context) throws IOException { cubeCapacity = CubeCapacity.valueOf(context.getConfiguration().get(BatchConstants.CUBE_CAPACITY)); switch (cubeCapacity) { case SMALL: - cut = FIVE_GIGA_BYTES; + cut = TEN_GIGA_BYTES; break; case MEDIUM: - cut = TEN_GIGA_BYTES; + cut = TWENTY_GIGA_BYTES; break; case LARGE: - cut = TWENTY_GIGA_BYTES; + cut = HUNDRED_GIGA_BYTES; break; } } diff --git a/job/src/main/java/com/kylinolap/job/hadoop/hive/JoinedFlatTableDesc.java b/job/src/main/java/com/kylinolap/job/hadoop/hive/JoinedFlatTableDesc.java index 2fdce8618..81adb2b5b 100644 --- a/job/src/main/java/com/kylinolap/job/hadoop/hive/JoinedFlatTableDesc.java +++ b/job/src/main/java/com/kylinolap/job/hadoop/hive/JoinedFlatTableDesc.java @@ -21,6 +21,7 @@ import java.util.Map; import com.kylinolap.cube.CubeSegment; +import com.kylinolap.cube.common.BytesSplitter; import com.kylinolap.cube.cuboid.Cuboid; import com.kylinolap.metadata.model.cube.CubeDesc; import com.kylinolap.metadata.model.cube.FunctionDesc; @@ -36,9 +37,12 @@ public class JoinedFlatTableDesc { private final CubeDesc cubeDesc; private final CubeSegment cubeSegment; + private List columnList = new ArrayList(); + private int[] rowKeyColumnIndexes; // the column index on flat table - private int[][] measureColumnIndexes; // [i] is the i.th measure related - // column index on flat table + private int[][] measureColumnIndexes; // [i] is the i.th measure related column index on flat table + + private int columnCount; public JoinedFlatTableDesc(CubeDesc cubeDesc, CubeSegment cubeSegment) { this.cubeDesc = cubeDesc; @@ -53,8 +57,6 @@ public CubeSegment getCubeSegment() { return cubeSegment; } - private List columnList = new ArrayList(); - public List getColumnList() { return columnList; } @@ -112,6 +114,8 @@ private void parseCubeDesc() { } } } + + this.columnCount = columnIndex; } private int contains(List columnList, TblColRef c) { @@ -139,6 +143,15 @@ public int[][] getMeasureColumnIndexes() { return measureColumnIndexes; } + // sanity check the input record (in bytes) matches what's expected + public void sanityCheck(BytesSplitter bytesSplitter) { + if (columnCount != bytesSplitter.getBufferSize()) { + throw new IllegalArgumentException("Expect " + columnCount + " columns, but see " + bytesSplitter.getBufferSize() + " -- " + bytesSplitter); + } + + // TODO: check data types here + } + public static class IntermediateColumnDesc { private String id; private String columnName; @@ -168,4 +181,5 @@ public String getTableName() { return tableName; } } + } diff --git a/metadata/src/main/java/com/kylinolap/metadata/MetadataManager.java b/metadata/src/main/java/com/kylinolap/metadata/MetadataManager.java index 8ce978ff3..72df0cfa9 100644 --- a/metadata/src/main/java/com/kylinolap/metadata/MetadataManager.java +++ b/metadata/src/main/java/com/kylinolap/metadata/MetadataManager.java @@ -273,7 +273,6 @@ private void reloadAllSourceTable() throws IOException { */ public static String loadSourceTableExd(ResourceStore store, String path, Map attrContainer) throws IOException { - logger.debug("Loading SourceTable exd " + path); InputStream is = store.getResource(path); if (is != null) { attrContainer.putAll(JsonUtil.readValue(is, HashMap.class)); @@ -290,7 +289,6 @@ public static String loadSourceTableExd(ResourceStore store, String path, Map 1.7 3.1.1 - 2.5.1 + 2.5.1 UTF-8 UTF-8 diff --git a/query/src/main/java/com/kylinolap/query/optrule/OLAPProjectRule.java b/query/src/main/java/com/kylinolap/query/optrule/OLAPProjectRule.java index e17436683..738cd213d 100644 --- a/query/src/main/java/com/kylinolap/query/optrule/OLAPProjectRule.java +++ b/query/src/main/java/com/kylinolap/query/optrule/OLAPProjectRule.java @@ -41,7 +41,8 @@ public void onMatch(RelOptRuleCall call) { ProjectRel project = call.rel(0); RelTraitSet traitSet = project.getTraitSet().replace(OLAPRel.CONVENTION); - OLAPProjectRel olapProj = new OLAPProjectRel(project.getCluster(), traitSet, convert(project.getChild(), project.getTraitSet().replace(OLAPRel.CONVENTION)), project.getProjects(), project.getRowType(), project.getFlags()); + OLAPProjectRel olapProj = new OLAPProjectRel(project.getCluster(), traitSet, // + convert(project.getChild(), traitSet), project.getProjects(), project.getRowType(), project.getFlags()); call.transformTo(olapProj); } diff --git a/query/src/main/java/com/kylinolap/query/relnode/OLAPAggregateRel.java b/query/src/main/java/com/kylinolap/query/relnode/OLAPAggregateRel.java index 33a403d6a..00fa1366d 100644 --- a/query/src/main/java/com/kylinolap/query/relnode/OLAPAggregateRel.java +++ b/query/src/main/java/com/kylinolap/query/relnode/OLAPAggregateRel.java @@ -247,8 +247,7 @@ private void translateAggregation() { } private void fillbackOptimizedColumn() { - // some aggcall will be optimized out in sub-query (e.g. tableau - // generated sql) + // some aggcall will be optimized out in sub-query (e.g. tableau generated sql) // we need to fill them back RelDataType inputAggRow = getChild().getRowType(); RelDataType outputAggRow = getRowType(); diff --git a/query/src/main/java/com/kylinolap/query/relnode/OLAPTableScan.java b/query/src/main/java/com/kylinolap/query/relnode/OLAPTableScan.java index 9c50c3670..23b4c1527 100644 --- a/query/src/main/java/com/kylinolap/query/relnode/OLAPTableScan.java +++ b/query/src/main/java/com/kylinolap/query/relnode/OLAPTableScan.java @@ -37,6 +37,7 @@ import org.eigenbase.relopt.RelOptTable; import org.eigenbase.relopt.RelTrait; import org.eigenbase.relopt.RelTraitSet; +import org.eigenbase.relopt.volcano.AbstractConverter.ExpandConversionRule; import org.eigenbase.reltype.RelDataType; import org.eigenbase.reltype.RelDataTypeFactory; import org.eigenbase.reltype.RelDataTypeField; @@ -124,6 +125,9 @@ public void register(RelOptPlanner planner) { planner.removeRule(PushFilterPastProjectRule.INSTANCE); // distinct count will be split into a separated query that is joined with the left query planner.removeRule(RemoveDistinctAggregateRule.INSTANCE); + + // see Dec 26th email @ http://mail-archives.apache.org/mod_mbox/calcite-dev/201412.mbox/browser + planner.removeRule(ExpandConversionRule.INSTANCE); } @Override diff --git a/query/src/test/resources/query/sql_tableau/query27.sql b/query/src/test/resources/query/sql_tableau/query27.sql index c118f34b9..724eb2df7 100644 --- a/query/src/test/resources/query/sql_tableau/query27.sql +++ b/query/src/test/resources/query/sql_tableau/query27.sql @@ -1,6 +1,6 @@ -SELECT "TEST_KYLIN_FACT"."CAL_DT", SUM("TEST_KYLIN_FACT"."PRICE") AS "sum_PRICE_ok" FROM "EDW"."TEST_KYLIN_FACT" "TEST_KYLIN_FACT" +SELECT "TEST_KYLIN_FACT"."CAL_DT", SUM("TEST_KYLIN_FACT"."PRICE") AS "sum_PRICE_ok" FROM "TEST_KYLIN_FACT" "TEST_KYLIN_FACT" INNER JOIN ( - SELECT COUNT(1) AS "XTableau_join_flag", SUM("TEST_KYLIN_FACT"."PRICE") AS "X__alias__A", "TEST_KYLIN_FACT"."CAL_DT" AS "none_CAL_DT_ok" FROM "EDW"."TEST_KYLIN_FACT" "TEST_KYLIN_FACT" + SELECT COUNT(1) AS "XTableau_join_flag", SUM("TEST_KYLIN_FACT"."PRICE") AS "X__alias__A", "TEST_KYLIN_FACT"."CAL_DT" AS "none_CAL_DT_ok" FROM "TEST_KYLIN_FACT" "TEST_KYLIN_FACT" GROUP BY "TEST_KYLIN_FACT"."CAL_DT" ORDER BY 2 DESC LIMIT 7 ) "t0" ON ("TEST_KYLIN_FACT"."CAL_DT" = "t0"."none_CAL_DT_ok") GROUP BY "TEST_KYLIN_FACT"."CAL_DT" diff --git a/server/pom.xml b/server/pom.xml index fea3397e0..94c996834 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -350,6 +350,11 @@ junit test + + com.thetransactioncompany + cors-filter + 1.3.2 + diff --git a/server/src/main/java/com/kylinolap/rest/controller/BasicController.java b/server/src/main/java/com/kylinolap/rest/controller/BasicController.java index e69691a75..b5f995608 100644 --- a/server/src/main/java/com/kylinolap/rest/controller/BasicController.java +++ b/server/src/main/java/com/kylinolap/rest/controller/BasicController.java @@ -27,7 +27,6 @@ import com.kylinolap.rest.exception.BadRequestException; import com.kylinolap.rest.exception.ForbiddenException; -import com.kylinolap.rest.exception.InternalErrorException; import com.kylinolap.rest.exception.NotFoundException; import com.kylinolap.rest.response.ErrorResponse; import com.kylinolap.rest.service.MetricsService; @@ -48,6 +47,7 @@ public class BasicController { @ExceptionHandler(Exception.class) @ResponseBody ErrorResponse handleError(HttpServletRequest req, Exception ex) { + logger.error("", ex); return new ErrorResponse(req.getRequestURL().toString(), ex); } @@ -70,6 +70,7 @@ ErrorResponse handleNotFound(HttpServletRequest req, Exception ex) { @ExceptionHandler(BadRequestException.class) @ResponseBody ErrorResponse handleBadRequest(HttpServletRequest req, Exception ex) { + logger.error("", ex); return new ErrorResponse(req.getRequestURL().toString(), ex); } } diff --git a/server/src/main/java/com/kylinolap/rest/controller/CubeController.java b/server/src/main/java/com/kylinolap/rest/controller/CubeController.java index d952ef4aa..d4318f5b7 100644 --- a/server/src/main/java/com/kylinolap/rest/controller/CubeController.java +++ b/server/src/main/java/com/kylinolap/rest/controller/CubeController.java @@ -24,13 +24,12 @@ import java.util.UUID; import org.apache.commons.lang.StringUtils; +import org.quartz.SchedulerException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -361,8 +360,8 @@ public CubeRequest updateCubeDesc(@RequestBody CubeRequest cubeRequest) throws J } catch (AccessDeniedException accessDeniedException) { throw new ForbiddenException("You don't have right to update this cube."); } catch (Exception e) { - logger.error("Failed to deal with the request.", e); - throw new InternalErrorException("Failed to deal with the request: " + e.getMessage()); + logger.error("Failed to deal with the request:"+e.getLocalizedMessage(), e); + throw new InternalErrorException("Failed to deal with the request: "+e.getLocalizedMessage() + e.getMessage()); } if (desc.getError().isEmpty()) { diff --git a/server/src/main/java/com/kylinolap/rest/controller/QueryController.java b/server/src/main/java/com/kylinolap/rest/controller/QueryController.java index e080affbd..9190c8eed 100644 --- a/server/src/main/java/com/kylinolap/rest/controller/QueryController.java +++ b/server/src/main/java/com/kylinolap/rest/controller/QueryController.java @@ -195,39 +195,38 @@ private SQLResponse doQuery(SQLRequest sqlRequest) { throw new InternalErrorException("Query is not allowed in " + serverMode + " mode."); } - if (sql.toLowerCase().contains("select")) { - SQLResponse sqlResponse = searchQueryInCache(sqlRequest); - try { - if (null == sqlResponse) { - sqlResponse = queryService.query(sqlRequest); - - long durationThreshold = KylinConfig.getInstanceFromEnv().getQueryDurationCacheThreshold(); - long scancountThreshold = KylinConfig.getInstanceFromEnv().getQueryScanCountCacheThreshold(); - if (!sqlResponse.getIsException() && (sqlResponse.getDuration() > durationThreshold || sqlResponse.getTotalScanCount() > scancountThreshold)) { - cacheManager.getCache(SUCCESS_QUERY_CACHE).put(new Element(sqlRequest, sqlResponse)); - } - } - - checkQueryAuth(sqlResponse); - - return sqlResponse; - } catch (AccessDeniedException ade) { - // Access exception is bind with each user, it will not be - // cached. - logger.error("Exception when execute sql", ade); - throw new ForbiddenException(ade.getLocalizedMessage()); - } catch (Exception e) { - SQLResponse exceptionRes = new SQLResponse(null, null, 0, true, e.getMessage()); - Cache exceptionCache = cacheManager.getCache(EXCEPTION_QUERY_CACHE); - exceptionCache.put(new Element(sqlRequest, exceptionRes)); - - logger.error("Exception when execute sql", e); - throw new InternalErrorException(QueryUtil.makeErrorMsgUserFriendly(e.getLocalizedMessage())); - } - } else { + if (sql.toLowerCase().contains("select") == false) { logger.debug("Directly return expection as not supported"); throw new InternalErrorException(QueryUtil.makeErrorMsgUserFriendly("Not Supported SQL.")); } + + SQLResponse sqlResponse = searchQueryInCache(sqlRequest); + try { + if (null == sqlResponse) { + sqlResponse = queryService.query(sqlRequest); + + long durationThreshold = KylinConfig.getInstanceFromEnv().getQueryDurationCacheThreshold(); + long scancountThreshold = KylinConfig.getInstanceFromEnv().getQueryScanCountCacheThreshold(); + if (!sqlResponse.getIsException() && (sqlResponse.getDuration() > durationThreshold || sqlResponse.getTotalScanCount() > scancountThreshold)) { + cacheManager.getCache(SUCCESS_QUERY_CACHE).put(new Element(sqlRequest, sqlResponse)); + } + } + + checkQueryAuth(sqlResponse); + + return sqlResponse; + } catch (AccessDeniedException ade) { + // Access exception is bind with each user, it will not be cached + logger.error("Exception when execute sql", ade); + throw new ForbiddenException(ade.getLocalizedMessage()); + } catch (Throwable e) { // calcite may throw AssertError + SQLResponse exceptionRes = new SQLResponse(null, null, 0, true, e.getMessage()); + Cache exceptionCache = cacheManager.getCache(EXCEPTION_QUERY_CACHE); + exceptionCache.put(new Element(sqlRequest, exceptionRes)); + + logger.error("Exception when execute sql", e); + throw new InternalErrorException(QueryUtil.makeErrorMsgUserFriendly(e.getLocalizedMessage())); + } } private SQLResponse searchQueryInCache(SQLRequest sqlRequest) { diff --git a/server/src/main/java/com/kylinolap/rest/security/CrossDomainFilter.java b/server/src/main/java/com/kylinolap/rest/security/CrossDomainFilter.java index 01d277b11..28feba6d2 100644 --- a/server/src/main/java/com/kylinolap/rest/security/CrossDomainFilter.java +++ b/server/src/main/java/com/kylinolap/rest/security/CrossDomainFilter.java @@ -51,7 +51,7 @@ public void init(FilterConfig filterConfig) throws ServletException { */ @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - if (Boolean.parseBoolean(KylinConfig.getInstanceFromEnv().getProperty("crossdomain.enable", "false"))) { + if (Boolean.parseBoolean(KylinConfig.getInstanceFromEnv().getProperty("crossdomain.enable", "true"))) { ((HttpServletResponse) response).addHeader("Access-Control-Allow-Origin", "*"); ((HttpServletResponse) response).addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); ((HttpServletResponse) response).addHeader("Access-Control-Allow-Headers", "Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With, Accept, Authorization"); diff --git a/server/src/main/java/com/kylinolap/rest/service/QueryService.java b/server/src/main/java/com/kylinolap/rest/service/QueryService.java index 9277a0856..87df549f5 100644 --- a/server/src/main/java/com/kylinolap/rest/service/QueryService.java +++ b/server/src/main/java/com/kylinolap/rest/service/QueryService.java @@ -240,6 +240,7 @@ public void logQuery(final SQLRequest request, final SQLResponse response, final stringBuilder.append("Total scan count: ").append(totalScanCount).append(newLine); stringBuilder.append("Result row count: ").append(resultRowCount).append(newLine); stringBuilder.append("Accept Partial: ").append(request.isAcceptPartial()).append(newLine); + stringBuilder.append("Is Partial Result: ").append(response.isPartial()).append(newLine); stringBuilder.append("Hit Cache: ").append(response.isHitCache()).append(newLine); stringBuilder.append("Message: ").append(response.getExceptionMessage()).append(newLine); stringBuilder.append("==========================[QUERY]===============================").append(newLine); diff --git a/server/src/main/webapp/WEB-INF/web.xml b/server/src/main/webapp/WEB-INF/web.xml index 490d8ffb7..190ae0bf0 100644 --- a/server/src/main/webapp/WEB-INF/web.xml +++ b/server/src/main/webapp/WEB-INF/web.xml @@ -45,14 +45,27 @@ - - crossDomainFilter - com.kylinolap.rest.security.CrossDomainFilter - - - crossDomainFilter - /* - + + CORS + com.thetransactioncompany.cors.CORSFilter + + cors.supportedHeaders + Authorization,Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With, Accept + + + cors.supportedMethods + GET, POST, PUT, DELETE, OPTIONS + + + cors.supportsCredentials + true + + + + + CORS + /* + +
{{num}}
+ +``` + +#### From the Controller + +```javascript +angular.module('yourAwesomeApp', ['angular-underscore']) +.controller('yourAwesomeCtrl', function($scope) { + $scope.sample([1, 2, 3]); // got 1, or 2, or 3. + $scope._([1, 2, 3]); // underscore's chain, http://underscorejs.org/#chain +}); + +``` + +### Local build + +``` +$ npm install uglify-js -g +$ uglifyjs angular-underscore.js > angular-underscore.min.js +``` + +## License + +(The MIT License) + +Copyright (c) 2014 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/webapp/app/components/angular-underscore/angular-underscore.js b/webapp/app/components/angular-underscore/angular-underscore.js new file mode 100644 index 000000000..55c9f41a9 --- /dev/null +++ b/webapp/app/components/angular-underscore/angular-underscore.js @@ -0,0 +1,157 @@ +(function (ng, _) { + 'use strict'; + + var + underscoreModule = ng.module('angular-underscore', []), + utilsModule = ng.module('angular-underscore/utils', []), + filtersModule = ng.module('angular-underscore/filters', []); + + // begin custom _ + + function propGetterFactory(prop) { + return function(obj) {return obj[prop];}; + } + + _._ = _; + + // Shiv "min", "max" ,"sortedIndex" to accept property predicate. + _.each(['min', 'max', 'sortedIndex'], function(fnName) { + _[fnName] = _.wrap(_[fnName], function(fn) { + var args = _.toArray(arguments).slice(1); + + if(_.isString(args[2])) { + // for "sortedIndex", transmuting str to property getter + args[2] = propGetterFactory(args[2]); + } + else if(_.isString(args[1])) { + // for "min" or "max", transmuting str to property getter + args[1] = propGetterFactory(args[1]); + } + + return fn.apply(_, args); + }); + }); + + // Shiv "filter", "reject" to angular's built-in, + // and reserve underscore's feature(works on obj). + ng.injector(['ng']).invoke(['$filter', function($filter) { + _.filter = _.select = _.wrap($filter('filter'), function(filter, obj, exp, comparator) { + if(!(_.isArray(obj))) { + obj = _.toArray(obj); + } + + return filter(obj, exp, comparator); + }); + + _.reject = function(obj, exp) { + // use angular built-in negated predicate + if(_.isString(exp)) { + return _.filter(obj, '!' + exp); + } + + var diff = _.bind(_.difference, _, obj); + + return diff(_.filter(obj, exp)); + }; + }]); + + // end custom _ + + + // begin register angular-underscore/utils + + _.each(_.methods(_), function(methodName) { + function register($rootScope) {$rootScope[methodName] = _.bind(_[methodName], _);} + + _.each([ + underscoreModule, + utilsModule, + ng.module('angular-underscore/utils/' + methodName, []) + ], function(module) { + module.run(['$rootScope', register]); + }); + }); + + // end register angular-underscore/utils + + + // begin register angular-underscore/filters + + var + adapList = [ + ['map', 'collect'], + ['reduce', 'inject', 'foldl'], + ['reduceRight', 'foldr'], + ['find', 'detect'], + ['filter', 'select'], + 'where', + 'findWhere', + 'reject', + 'invoke', + 'pluck', + 'max', + 'min', + 'sortBy', + 'groupBy', + 'indexBy', + 'countBy', + 'shuffle', + 'sample', + 'toArray', + 'size', + ['first', 'head', 'take'], + 'initial', + 'last', + ['rest', 'tail', 'drop'], + 'compact', + 'flatten', + 'without', + 'partition', + 'union', + 'intersection', + 'difference', + ['uniq', 'unique'], + 'zip', + 'object', + 'indexOf', + 'lastIndexOf', + 'sortedIndex', + 'keys', + 'values', + 'pairs', + 'invert', + ['functions', 'methods'], + 'pick', + 'omit', + 'tap', + 'identity', + 'uniqueId', + 'escape', + 'unescape', + 'result', + 'template' + ]; + + _.each(adapList, function(filterNames) { + if(!(_.isArray(filterNames))) { + filterNames = [filterNames]; + } + + var + filter = _.bind(_[filterNames[0]], _), + filterFactory = function() {return filter;}; + + _.each(filterNames, function(filterName) { + _.each([ + underscoreModule, + filtersModule, + ng.module('angular-underscore/filters/' + filterName, []) + ], function(module) { + module.filter(filterName, filterFactory); + }); + }); + }); + + // end register angular-underscore/filters + +}(angular, _)); diff --git a/webapp/app/components/angular-underscore/angular-underscore.min.js b/webapp/app/components/angular-underscore/angular-underscore.min.js new file mode 100644 index 000000000..5a087b1f8 --- /dev/null +++ b/webapp/app/components/angular-underscore/angular-underscore.min.js @@ -0,0 +1 @@ +(function(ng,_){"use strict";var underscoreModule=ng.module("angular-underscore",[]),utilsModule=ng.module("angular-underscore/utils",[]),filtersModule=ng.module("angular-underscore/filters",[]);function propGetterFactory(prop){return function(obj){return obj[prop]}}_._=_;_.each(["min","max","sortedIndex"],function(fnName){_[fnName]=_.wrap(_[fnName],function(fn){var args=_.toArray(arguments).slice(1);if(_.isString(args[2])){args[2]=propGetterFactory(args[2])}else if(_.isString(args[1])){args[1]=propGetterFactory(args[1])}return fn.apply(_,args)})});ng.injector(["ng"]).invoke(["$filter",function($filter){_.filter=_.select=_.wrap($filter("filter"),function(filter,obj,exp){if(!_.isArray(obj)){obj=_.toArray(obj)}return filter(obj,exp)});_.reject=function(obj,exp){if(_.isString(exp)){return _.filter(obj,"!"+exp)}var diff=_.bind(_.difference,_,obj);return diff(_.filter(obj,exp))}}]);_.each(_.methods(_),function(methodName){function register($rootScope){$rootScope[methodName]=_.bind(_[methodName],_)}_.each([underscoreModule,utilsModule,ng.module("angular-underscore/utils/"+methodName,[])],function(module){module.run(["$rootScope",register])})});var adapList=[["map","collect"],["reduce","inject","foldl"],["reduceRight","foldr"],["find","detect"],["filter","select"],"where","findWhere","reject","invoke","pluck","max","min","sortBy","groupBy","countBy","shuffle","toArray","size",["first","head","take"],"initial","last",["rest","tail","drop"],"compact","flatten","without","union","intersection","difference",["uniq","unique"],"zip","object","indexOf","lastIndexOf","sortedIndex","keys","values","pairs","invert",["functions","methods"],"pick","omit","tap","identity","uniqueId","escape","result","template"];_.each(adapList,function(filterNames){if(!_.isArray(filterNames)){filterNames=[filterNames]}var filter=_.bind(_[filterNames[0]],_),filterFactory=function(){return filter};_.each(filterNames,function(filterName){_.each([underscoreModule,filtersModule,ng.module("angular-underscore/filters/"+filterName,[])],function(module){module.filter(filterName,filterFactory)})})})})(angular,_); \ No newline at end of file diff --git a/webapp/app/components/angular-underscore/bower.json b/webapp/app/components/angular-underscore/bower.json new file mode 100644 index 000000000..04c26d749 --- /dev/null +++ b/webapp/app/components/angular-underscore/bower.json @@ -0,0 +1,12 @@ +{ + "name": "angular-underscore", + "version": "0.5.0", + "main": "angular-underscore.js", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ] +} diff --git a/webapp/app/components/angular-underscore/package.json b/webapp/app/components/angular-underscore/package.json new file mode 100644 index 000000000..7095c5cf2 --- /dev/null +++ b/webapp/app/components/angular-underscore/package.json @@ -0,0 +1,11 @@ +{ + "name": "angular-underscore", + "description": "Underscore adapter for AngularJS.", + "version": "0.5.0", + "author": "floydsoft ", + "main": "angular-underscore.js", + "repository": { + "type": "git", + "url": "https://github.com/floydsoft/angular-underscore" + } +} diff --git a/webapp/app/components/underscore/.bower.json b/webapp/app/components/underscore/.bower.json new file mode 100644 index 000000000..1ad7bf10c --- /dev/null +++ b/webapp/app/components/underscore/.bower.json @@ -0,0 +1,32 @@ +{ + "name": "underscore", + "version": "1.7.0", + "main": "underscore.js", + "keywords": [ + "util", + "functional", + "server", + "client", + "browser" + ], + "ignore": [ + "docs", + "test", + "*.yml", + "CNAME", + "index.html", + "favicon.ico", + "CONTRIBUTING.md" + ], + "homepage": "https://github.com/jashkenas/underscore", + "_release": "1.7.0", + "_resolution": { + "type": "version", + "tag": "1.7.0", + "commit": "da996e665deb0b69b257e80e3e257c04fde4191c" + }, + "_source": "git://github.com/jashkenas/underscore.git", + "_target": "~1.7.0", + "_originalSource": "underscore", + "_direct": true +} \ No newline at end of file diff --git a/webapp/app/components/underscore/.eslintrc b/webapp/app/components/underscore/.eslintrc new file mode 100644 index 000000000..2c46d63de --- /dev/null +++ b/webapp/app/components/underscore/.eslintrc @@ -0,0 +1,35 @@ +{ + "env": { + "browser": true, + "node": true, + "amd": true + }, + + "rules": { + "brace-style": [1, "1tbs"], + "curly": [0, "multi"], + "eqeqeq": [1, "smart"], + "max-depth": [1, 4], + "max-params": [1, 5], + "new-cap": 2, + "new-parens": 0, + "no-constant-condition": 0, + "no-div-regex": 1, + "no-else-return": 1, + "no-extra-parens": 1, + "no-floating-decimal": 2, + "no-inner-declarations": 2, + "no-lonely-if": 1, + "no-nested-ternary": 2, + "no-new-object": 0, + "no-new-func": 0, + "no-underscore-dangle": 0, + "quotes": [2, "single", "avoid-escape"], + "radix": 2, + "space-after-keywords": [2, "always"], + "space-in-brackets": [2, "never"], + "space-unary-word-ops": 2, + "strict": 0, + "wrap-iife": 2 + } +} diff --git a/webapp/app/components/underscore/.gitignore b/webapp/app/components/underscore/.gitignore new file mode 100644 index 000000000..029616c3c --- /dev/null +++ b/webapp/app/components/underscore/.gitignore @@ -0,0 +1,2 @@ +raw +node_modules diff --git a/webapp/app/components/underscore/LICENSE b/webapp/app/components/underscore/LICENSE new file mode 100644 index 000000000..0d6b8739d --- /dev/null +++ b/webapp/app/components/underscore/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative +Reporters & Editors + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/webapp/app/components/underscore/README.md b/webapp/app/components/underscore/README.md new file mode 100644 index 000000000..c2ba2590c --- /dev/null +++ b/webapp/app/components/underscore/README.md @@ -0,0 +1,22 @@ + __ + /\ \ __ + __ __ ___ \_\ \ __ _ __ ____ ___ ___ _ __ __ /\_\ ____ + /\ \/\ \ /' _ `\ /'_ \ /'__`\/\ __\/ ,__\ / ___\ / __`\/\ __\/'__`\ \/\ \ /',__\ + \ \ \_\ \/\ \/\ \/\ \ \ \/\ __/\ \ \//\__, `\/\ \__//\ \ \ \ \ \//\ __/ __ \ \ \/\__, `\ + \ \____/\ \_\ \_\ \___,_\ \____\\ \_\\/\____/\ \____\ \____/\ \_\\ \____\/\_\ _\ \ \/\____/ + \/___/ \/_/\/_/\/__,_ /\/____/ \/_/ \/___/ \/____/\/___/ \/_/ \/____/\/_//\ \_\ \/___/ + \ \____/ + \/___/ + +Underscore.js is a utility-belt library for JavaScript that provides +support for the usual functional suspects (each, map, reduce, filter...) +without extending any core JavaScript objects. + +For Docs, License, Tests, and pre-packed downloads, see: +http://underscorejs.org + +Underscore is an open-sourced component of DocumentCloud: +https://github.com/documentcloud + +Many thanks to our contributors: +https://github.com/jashkenas/underscore/contributors diff --git a/webapp/app/components/underscore/bower.json b/webapp/app/components/underscore/bower.json new file mode 100644 index 000000000..82acb5c77 --- /dev/null +++ b/webapp/app/components/underscore/bower.json @@ -0,0 +1,7 @@ +{ + "name": "underscore", + "version": "1.7.0", + "main": "underscore.js", + "keywords": ["util", "functional", "server", "client", "browser"], + "ignore" : ["docs", "test", "*.yml", "CNAME", "index.html", "favicon.ico", "CONTRIBUTING.md"] +} diff --git a/webapp/app/components/underscore/component.json b/webapp/app/components/underscore/component.json new file mode 100644 index 000000000..47d145086 --- /dev/null +++ b/webapp/app/components/underscore/component.json @@ -0,0 +1,10 @@ +{ + "name" : "underscore", + "description" : "JavaScript's functional programming helper library.", + "keywords" : ["util", "functional", "server", "client", "browser"], + "repo" : "jashkenas/underscore", + "main" : "underscore.js", + "scripts" : ["underscore.js"], + "version" : "1.7.0", + "license" : "MIT" +} diff --git a/webapp/app/components/underscore/package.json b/webapp/app/components/underscore/package.json new file mode 100644 index 000000000..f01eb604a --- /dev/null +++ b/webapp/app/components/underscore/package.json @@ -0,0 +1,41 @@ +{ + "name": "underscore", + "description": "JavaScript's functional programming helper library.", + "homepage": "http://underscorejs.org", + "keywords": [ + "util", + "functional", + "server", + "client", + "browser" + ], + "author": "Jeremy Ashkenas ", + "repository": { + "type": "git", + "url": "git://github.com/jashkenas/underscore.git" + }, + "main": "underscore.js", + "version": "1.7.0", + "devDependencies": { + "docco": "0.6.x", + "phantomjs": "1.9.7-1", + "uglify-js": "2.4.x", + "eslint": "0.6.x" + }, + "scripts": { + "test": "phantomjs test/vendor/runner.js test/index.html?noglobals=true && eslint underscore.js test/*.js test/vendor/runner.js", + "build": "uglifyjs underscore.js -c \"evaluate=false\" --comments \"/ .*/\" -m --source-map underscore-min.map -o underscore-min.js", + "doc": "docco underscore.js" + }, + "licenses": [ + { + "type": "MIT", + "url": "https://raw.github.com/jashkenas/underscore/master/LICENSE" + } + ], + "files": [ + "underscore.js", + "underscore-min.js", + "LICENSE" + ] +} diff --git a/webapp/app/components/underscore/underscore-min.js b/webapp/app/components/underscore/underscore-min.js new file mode 100644 index 000000000..11f1d96f5 --- /dev/null +++ b/webapp/app/components/underscore/underscore-min.js @@ -0,0 +1,6 @@ +// Underscore.js 1.7.0 +// http://underscorejs.org +// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. +(function(){var n=this,t=n._,r=Array.prototype,e=Object.prototype,u=Function.prototype,i=r.push,a=r.slice,o=r.concat,l=e.toString,c=e.hasOwnProperty,f=Array.isArray,s=Object.keys,p=u.bind,h=function(n){return n instanceof h?n:this instanceof h?void(this._wrapped=n):new h(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=h),exports._=h):n._=h,h.VERSION="1.7.0";var g=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}};h.iteratee=function(n,t,r){return null==n?h.identity:h.isFunction(n)?g(n,t,r):h.isObject(n)?h.matches(n):h.property(n)},h.each=h.forEach=function(n,t,r){if(null==n)return n;t=g(t,r);var e,u=n.length;if(u===+u)for(e=0;u>e;e++)t(n[e],e,n);else{var i=h.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},h.map=h.collect=function(n,t,r){if(null==n)return[];t=h.iteratee(t,r);for(var e,u=n.length!==+n.length&&h.keys(n),i=(u||n).length,a=Array(i),o=0;i>o;o++)e=u?u[o]:o,a[o]=t(n[e],e,n);return a};var v="Reduce of empty array with no initial value";h.reduce=h.foldl=h.inject=function(n,t,r,e){null==n&&(n=[]),t=g(t,e,4);var u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length,o=0;if(arguments.length<3){if(!a)throw new TypeError(v);r=n[i?i[o++]:o++]}for(;a>o;o++)u=i?i[o]:o,r=t(r,n[u],u,n);return r},h.reduceRight=h.foldr=function(n,t,r,e){null==n&&(n=[]),t=g(t,e,4);var u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;if(arguments.length<3){if(!a)throw new TypeError(v);r=n[i?i[--a]:--a]}for(;a--;)u=i?i[a]:a,r=t(r,n[u],u,n);return r},h.find=h.detect=function(n,t,r){var e;return t=h.iteratee(t,r),h.some(n,function(n,r,u){return t(n,r,u)?(e=n,!0):void 0}),e},h.filter=h.select=function(n,t,r){var e=[];return null==n?e:(t=h.iteratee(t,r),h.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e)},h.reject=function(n,t,r){return h.filter(n,h.negate(h.iteratee(t)),r)},h.every=h.all=function(n,t,r){if(null==n)return!0;t=h.iteratee(t,r);var e,u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;for(e=0;a>e;e++)if(u=i?i[e]:e,!t(n[u],u,n))return!1;return!0},h.some=h.any=function(n,t,r){if(null==n)return!1;t=h.iteratee(t,r);var e,u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;for(e=0;a>e;e++)if(u=i?i[e]:e,t(n[u],u,n))return!0;return!1},h.contains=h.include=function(n,t){return null==n?!1:(n.length!==+n.length&&(n=h.values(n)),h.indexOf(n,t)>=0)},h.invoke=function(n,t){var r=a.call(arguments,2),e=h.isFunction(t);return h.map(n,function(n){return(e?t:n[t]).apply(n,r)})},h.pluck=function(n,t){return h.map(n,h.property(t))},h.where=function(n,t){return h.filter(n,h.matches(t))},h.findWhere=function(n,t){return h.find(n,h.matches(t))},h.max=function(n,t,r){var e,u,i=-1/0,a=-1/0;if(null==t&&null!=n){n=n.length===+n.length?n:h.values(n);for(var o=0,l=n.length;l>o;o++)e=n[o],e>i&&(i=e)}else t=h.iteratee(t,r),h.each(n,function(n,r,e){u=t(n,r,e),(u>a||u===-1/0&&i===-1/0)&&(i=n,a=u)});return i},h.min=function(n,t,r){var e,u,i=1/0,a=1/0;if(null==t&&null!=n){n=n.length===+n.length?n:h.values(n);for(var o=0,l=n.length;l>o;o++)e=n[o],i>e&&(i=e)}else t=h.iteratee(t,r),h.each(n,function(n,r,e){u=t(n,r,e),(a>u||1/0===u&&1/0===i)&&(i=n,a=u)});return i},h.shuffle=function(n){for(var t,r=n&&n.length===+n.length?n:h.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=h.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},h.sample=function(n,t,r){return null==t||r?(n.length!==+n.length&&(n=h.values(n)),n[h.random(n.length-1)]):h.shuffle(n).slice(0,Math.max(0,t))},h.sortBy=function(n,t,r){return t=h.iteratee(t,r),h.pluck(h.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var m=function(n){return function(t,r,e){var u={};return r=h.iteratee(r,e),h.each(t,function(e,i){var a=r(e,i,t);n(u,e,a)}),u}};h.groupBy=m(function(n,t,r){h.has(n,r)?n[r].push(t):n[r]=[t]}),h.indexBy=m(function(n,t,r){n[r]=t}),h.countBy=m(function(n,t,r){h.has(n,r)?n[r]++:n[r]=1}),h.sortedIndex=function(n,t,r,e){r=h.iteratee(r,e,1);for(var u=r(t),i=0,a=n.length;a>i;){var o=i+a>>>1;r(n[o])t?[]:a.call(n,0,t)},h.initial=function(n,t,r){return a.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},h.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:a.call(n,Math.max(n.length-t,0))},h.rest=h.tail=h.drop=function(n,t,r){return a.call(n,null==t||r?1:t)},h.compact=function(n){return h.filter(n,h.identity)};var y=function(n,t,r,e){if(t&&h.every(n,h.isArray))return o.apply(e,n);for(var u=0,a=n.length;a>u;u++){var l=n[u];h.isArray(l)||h.isArguments(l)?t?i.apply(e,l):y(l,t,r,e):r||e.push(l)}return e};h.flatten=function(n,t){return y(n,t,!1,[])},h.without=function(n){return h.difference(n,a.call(arguments,1))},h.uniq=h.unique=function(n,t,r,e){if(null==n)return[];h.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=h.iteratee(r,e));for(var u=[],i=[],a=0,o=n.length;o>a;a++){var l=n[a];if(t)a&&i===l||u.push(l),i=l;else if(r){var c=r(l,a,n);h.indexOf(i,c)<0&&(i.push(c),u.push(l))}else h.indexOf(u,l)<0&&u.push(l)}return u},h.union=function(){return h.uniq(y(arguments,!0,!0,[]))},h.intersection=function(n){if(null==n)return[];for(var t=[],r=arguments.length,e=0,u=n.length;u>e;e++){var i=n[e];if(!h.contains(t,i)){for(var a=1;r>a&&h.contains(arguments[a],i);a++);a===r&&t.push(i)}}return t},h.difference=function(n){var t=y(a.call(arguments,1),!0,!0,[]);return h.filter(n,function(n){return!h.contains(t,n)})},h.zip=function(n){if(null==n)return[];for(var t=h.max(arguments,"length").length,r=Array(t),e=0;t>e;e++)r[e]=h.pluck(arguments,e);return r},h.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},h.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=h.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}for(;u>e;e++)if(n[e]===t)return e;return-1},h.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=n.length;for("number"==typeof r&&(e=0>r?e+r+1:Math.min(e,r+1));--e>=0;)if(n[e]===t)return e;return-1},h.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=r||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=Array(e),i=0;e>i;i++,n+=r)u[i]=n;return u};var d=function(){};h.bind=function(n,t){var r,e;if(p&&n.bind===p)return p.apply(n,a.call(arguments,1));if(!h.isFunction(n))throw new TypeError("Bind must be called on a function");return r=a.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(a.call(arguments)));d.prototype=n.prototype;var u=new d;d.prototype=null;var i=n.apply(u,r.concat(a.call(arguments)));return h.isObject(i)?i:u}},h.partial=function(n){var t=a.call(arguments,1);return function(){for(var r=0,e=t.slice(),u=0,i=e.length;i>u;u++)e[u]===h&&(e[u]=arguments[r++]);for(;r=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=h.bind(n[r],n);return n},h.memoize=function(n,t){var r=function(e){var u=r.cache,i=t?t.apply(this,arguments):e;return h.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},h.delay=function(n,t){var r=a.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},h.defer=function(n){return h.delay.apply(h,[n,1].concat(a.call(arguments,1)))},h.throttle=function(n,t,r){var e,u,i,a=null,o=0;r||(r={});var l=function(){o=r.leading===!1?0:h.now(),a=null,i=n.apply(e,u),a||(e=u=null)};return function(){var c=h.now();o||r.leading!==!1||(o=c);var f=t-(c-o);return e=this,u=arguments,0>=f||f>t?(clearTimeout(a),a=null,o=c,i=n.apply(e,u),a||(e=u=null)):a||r.trailing===!1||(a=setTimeout(l,f)),i}},h.debounce=function(n,t,r){var e,u,i,a,o,l=function(){var c=h.now()-a;t>c&&c>0?e=setTimeout(l,t-c):(e=null,r||(o=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,a=h.now();var c=r&&!e;return e||(e=setTimeout(l,t)),c&&(o=n.apply(i,u),i=u=null),o}},h.wrap=function(n,t){return h.partial(t,n)},h.negate=function(n){return function(){return!n.apply(this,arguments)}},h.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},h.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},h.before=function(n,t){var r;return function(){return--n>0?r=t.apply(this,arguments):t=null,r}},h.once=h.partial(h.before,2),h.keys=function(n){if(!h.isObject(n))return[];if(s)return s(n);var t=[];for(var r in n)h.has(n,r)&&t.push(r);return t},h.values=function(n){for(var t=h.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},h.pairs=function(n){for(var t=h.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},h.invert=function(n){for(var t={},r=h.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},h.functions=h.methods=function(n){var t=[];for(var r in n)h.isFunction(n[r])&&t.push(r);return t.sort()},h.extend=function(n){if(!h.isObject(n))return n;for(var t,r,e=1,u=arguments.length;u>e;e++){t=arguments[e];for(r in t)c.call(t,r)&&(n[r]=t[r])}return n},h.pick=function(n,t,r){var e,u={};if(null==n)return u;if(h.isFunction(t)){t=g(t,r);for(e in n){var i=n[e];t(i,e,n)&&(u[e]=i)}}else{var l=o.apply([],a.call(arguments,1));n=new Object(n);for(var c=0,f=l.length;f>c;c++)e=l[c],e in n&&(u[e]=n[e])}return u},h.omit=function(n,t,r){if(h.isFunction(t))t=h.negate(t);else{var e=h.map(o.apply([],a.call(arguments,1)),String);t=function(n,t){return!h.contains(e,t)}}return h.pick(n,t,r)},h.defaults=function(n){if(!h.isObject(n))return n;for(var t=1,r=arguments.length;r>t;t++){var e=arguments[t];for(var u in e)n[u]===void 0&&(n[u]=e[u])}return n},h.clone=function(n){return h.isObject(n)?h.isArray(n)?n.slice():h.extend({},n):n},h.tap=function(n,t){return t(n),n};var b=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof h&&(n=n._wrapped),t instanceof h&&(t=t._wrapped);var u=l.call(n);if(u!==l.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]===n)return e[i]===t;var a=n.constructor,o=t.constructor;if(a!==o&&"constructor"in n&&"constructor"in t&&!(h.isFunction(a)&&a instanceof a&&h.isFunction(o)&&o instanceof o))return!1;r.push(n),e.push(t);var c,f;if("[object Array]"===u){if(c=n.length,f=c===t.length)for(;c--&&(f=b(n[c],t[c],r,e)););}else{var s,p=h.keys(n);if(c=p.length,f=h.keys(t).length===c)for(;c--&&(s=p[c],f=h.has(t,s)&&b(n[s],t[s],r,e)););}return r.pop(),e.pop(),f};h.isEqual=function(n,t){return b(n,t,[],[])},h.isEmpty=function(n){if(null==n)return!0;if(h.isArray(n)||h.isString(n)||h.isArguments(n))return 0===n.length;for(var t in n)if(h.has(n,t))return!1;return!0},h.isElement=function(n){return!(!n||1!==n.nodeType)},h.isArray=f||function(n){return"[object Array]"===l.call(n)},h.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},h.each(["Arguments","Function","String","Number","Date","RegExp"],function(n){h["is"+n]=function(t){return l.call(t)==="[object "+n+"]"}}),h.isArguments(arguments)||(h.isArguments=function(n){return h.has(n,"callee")}),"function"!=typeof/./&&(h.isFunction=function(n){return"function"==typeof n||!1}),h.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},h.isNaN=function(n){return h.isNumber(n)&&n!==+n},h.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===l.call(n)},h.isNull=function(n){return null===n},h.isUndefined=function(n){return n===void 0},h.has=function(n,t){return null!=n&&c.call(n,t)},h.noConflict=function(){return n._=t,this},h.identity=function(n){return n},h.constant=function(n){return function(){return n}},h.noop=function(){},h.property=function(n){return function(t){return t[n]}},h.matches=function(n){var t=h.pairs(n),r=t.length;return function(n){if(null==n)return!r;n=new Object(n);for(var e=0;r>e;e++){var u=t[e],i=u[0];if(u[1]!==n[i]||!(i in n))return!1}return!0}},h.times=function(n,t,r){var e=Array(Math.max(0,n));t=g(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},h.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},h.now=Date.now||function(){return(new Date).getTime()};var _={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},w=h.invert(_),j=function(n){var t=function(t){return n[t]},r="(?:"+h.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};h.escape=j(_),h.unescape=j(w),h.result=function(n,t){if(null==n)return void 0;var r=n[t];return h.isFunction(r)?n[t]():r};var x=0;h.uniqueId=function(n){var t=++x+"";return n?n+t:t},h.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var A=/(.)^/,k={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},O=/\\|'|\r|\n|\u2028|\u2029/g,F=function(n){return"\\"+k[n]};h.template=function(n,t,r){!t&&r&&(t=r),t=h.defaults({},t,h.templateSettings);var e=RegExp([(t.escape||A).source,(t.interpolate||A).source,(t.evaluate||A).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,a,o){return i+=n.slice(u,o).replace(O,F),u=o+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":a&&(i+="';\n"+a+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var a=new Function(t.variable||"obj","_",i)}catch(o){throw o.source=i,o}var l=function(n){return a.call(this,n,h)},c=t.variable||"obj";return l.source="function("+c+"){\n"+i+"}",l},h.chain=function(n){var t=h(n);return t._chain=!0,t};var E=function(n){return this._chain?h(n).chain():n};h.mixin=function(n){h.each(h.functions(n),function(t){var r=h[t]=n[t];h.prototype[t]=function(){var n=[this._wrapped];return i.apply(n,arguments),E.call(this,r.apply(h,n))}})},h.mixin(h),h.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=r[n];h.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],E.call(this,r)}}),h.each(["concat","join","slice"],function(n){var t=r[n];h.prototype[n]=function(){return E.call(this,t.apply(this._wrapped,arguments))}}),h.prototype.value=function(){return this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return h})}).call(this); +//# sourceMappingURL=underscore-min.map \ No newline at end of file diff --git a/webapp/app/components/underscore/underscore-min.map b/webapp/app/components/underscore/underscore-min.map new file mode 100644 index 000000000..73c951e50 --- /dev/null +++ b/webapp/app/components/underscore/underscore-min.map @@ -0,0 +1 @@ +{"version":3,"file":"underscore-min.js","sources":["underscore.js"],"names":["root","this","previousUnderscore","_","ArrayProto","Array","prototype","ObjProto","Object","FuncProto","Function","push","slice","concat","toString","hasOwnProperty","nativeIsArray","isArray","nativeKeys","keys","nativeBind","bind","obj","_wrapped","exports","module","VERSION","createCallback","func","context","argCount","value","call","other","index","collection","accumulator","apply","arguments","iteratee","identity","isFunction","isObject","matches","property","each","forEach","i","length","map","collect","currentKey","results","reduceError","reduce","foldl","inject","memo","TypeError","reduceRight","foldr","find","detect","predicate","result","some","list","filter","select","reject","negate","every","all","any","contains","include","target","values","indexOf","invoke","method","args","isFunc","pluck","key","where","attrs","findWhere","max","computed","Infinity","lastComputed","min","shuffle","rand","set","shuffled","random","sample","n","guard","Math","sortBy","criteria","sort","left","right","a","b","group","behavior","groupBy","has","indexBy","countBy","sortedIndex","array","low","high","mid","toArray","size","partition","pass","fail","first","head","take","initial","last","rest","tail","drop","compact","flatten","input","shallow","strict","output","isArguments","without","difference","uniq","unique","isSorted","isBoolean","seen","union","intersection","argsLength","item","j","zip","object","lastIndexOf","from","idx","range","start","stop","step","ceil","Ctor","bound","self","partial","boundArgs","position","bindAll","Error","memoize","hasher","cache","address","delay","wait","setTimeout","defer","throttle","options","timeout","previous","later","leading","now","remaining","clearTimeout","trailing","debounce","immediate","timestamp","callNow","wrap","wrapper","compose","after","times","before","once","pairs","invert","functions","methods","names","extend","source","prop","pick","omit","String","defaults","clone","tap","interceptor","eq","aStack","bStack","className","aCtor","constructor","bCtor","pop","isEqual","isEmpty","isString","isElement","nodeType","type","name","isFinite","isNaN","parseFloat","isNumber","isNull","isUndefined","noConflict","constant","noop","pair","accum","floor","Date","getTime","escapeMap","&","<",">","\"","'","`","unescapeMap","createEscaper","escaper","match","join","testRegexp","RegExp","replaceRegexp","string","test","replace","escape","unescape","idCounter","uniqueId","prefix","id","templateSettings","evaluate","interpolate","noMatch","escapes","\\","\r","\n","
","
","escapeChar","template","text","settings","oldSettings","matcher","offset","variable","render","e","data","argument","chain","instance","_chain","mixin","define","amd"],"mappings":"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"} \ No newline at end of file diff --git a/webapp/app/components/underscore/underscore.js b/webapp/app/components/underscore/underscore.js new file mode 100644 index 000000000..b4f49a020 --- /dev/null +++ b/webapp/app/components/underscore/underscore.js @@ -0,0 +1,1415 @@ +// Underscore.js 1.7.0 +// http://underscorejs.org +// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `exports` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var + push = ArrayProto.push, + slice = ArrayProto.slice, + concat = ArrayProto.concat, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.7.0'; + + // Internal function that returns an efficient (for current engines) version + // of the passed-in callback, to be repeatedly applied in other Underscore + // functions. + var createCallback = function(func, context, argCount) { + if (context === void 0) return func; + switch (argCount == null ? 3 : argCount) { + case 1: return function(value) { + return func.call(context, value); + }; + case 2: return function(value, other) { + return func.call(context, value, other); + }; + case 3: return function(value, index, collection) { + return func.call(context, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(context, accumulator, value, index, collection); + }; + } + return function() { + return func.apply(context, arguments); + }; + }; + + // A mostly-internal function to generate callbacks that can be applied + // to each element in a collection, returning the desired result — either + // identity, an arbitrary callback, a property matcher, or a property accessor. + _.iteratee = function(value, context, argCount) { + if (value == null) return _.identity; + if (_.isFunction(value)) return createCallback(value, context, argCount); + if (_.isObject(value)) return _.matches(value); + return _.property(value); + }; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles raw objects in addition to array-likes. Treats all + // sparse array-likes as if they were dense. + _.each = _.forEach = function(obj, iteratee, context) { + if (obj == null) return obj; + iteratee = createCallback(iteratee, context); + var i, length = obj.length; + if (length === +length) { + for (i = 0; i < length; i++) { + iteratee(obj[i], i, obj); + } + } else { + var keys = _.keys(obj); + for (i = 0, length = keys.length; i < length; i++) { + iteratee(obj[keys[i]], keys[i], obj); + } + } + return obj; + }; + + // Return the results of applying the iteratee to each element. + _.map = _.collect = function(obj, iteratee, context) { + if (obj == null) return []; + iteratee = _.iteratee(iteratee, context); + var keys = obj.length !== +obj.length && _.keys(obj), + length = (keys || obj).length, + results = Array(length), + currentKey; + for (var index = 0; index < length; index++) { + currentKey = keys ? keys[index] : index; + results[index] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + }; + + var reduceError = 'Reduce of empty array with no initial value'; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. + _.reduce = _.foldl = _.inject = function(obj, iteratee, memo, context) { + if (obj == null) obj = []; + iteratee = createCallback(iteratee, context, 4); + var keys = obj.length !== +obj.length && _.keys(obj), + length = (keys || obj).length, + index = 0, currentKey; + if (arguments.length < 3) { + if (!length) throw new TypeError(reduceError); + memo = obj[keys ? keys[index++] : index++]; + } + for (; index < length; index++) { + currentKey = keys ? keys[index] : index; + memo = iteratee(memo, obj[currentKey], currentKey, obj); + } + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + _.reduceRight = _.foldr = function(obj, iteratee, memo, context) { + if (obj == null) obj = []; + iteratee = createCallback(iteratee, context, 4); + var keys = obj.length !== + obj.length && _.keys(obj), + index = (keys || obj).length, + currentKey; + if (arguments.length < 3) { + if (!index) throw new TypeError(reduceError); + memo = obj[keys ? keys[--index] : --index]; + } + while (index--) { + currentKey = keys ? keys[index] : index; + memo = iteratee(memo, obj[currentKey], currentKey, obj); + } + return memo; + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, predicate, context) { + var result; + predicate = _.iteratee(predicate, context); + _.some(obj, function(value, index, list) { + if (predicate(value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Aliased as `select`. + _.filter = _.select = function(obj, predicate, context) { + var results = []; + if (obj == null) return results; + predicate = _.iteratee(predicate, context); + _.each(obj, function(value, index, list) { + if (predicate(value, index, list)) results.push(value); + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, predicate, context) { + return _.filter(obj, _.negate(_.iteratee(predicate)), context); + }; + + // Determine whether all of the elements match a truth test. + // Aliased as `all`. + _.every = _.all = function(obj, predicate, context) { + if (obj == null) return true; + predicate = _.iteratee(predicate, context); + var keys = obj.length !== +obj.length && _.keys(obj), + length = (keys || obj).length, + index, currentKey; + for (index = 0; index < length; index++) { + currentKey = keys ? keys[index] : index; + if (!predicate(obj[currentKey], currentKey, obj)) return false; + } + return true; + }; + + // Determine if at least one element in the object matches a truth test. + // Aliased as `any`. + _.some = _.any = function(obj, predicate, context) { + if (obj == null) return false; + predicate = _.iteratee(predicate, context); + var keys = obj.length !== +obj.length && _.keys(obj), + length = (keys || obj).length, + index, currentKey; + for (index = 0; index < length; index++) { + currentKey = keys ? keys[index] : index; + if (predicate(obj[currentKey], currentKey, obj)) return true; + } + return false; + }; + + // Determine if the array or object contains a given value (using `===`). + // Aliased as `include`. + _.contains = _.include = function(obj, target) { + if (obj == null) return false; + if (obj.length !== +obj.length) obj = _.values(obj); + return _.indexOf(obj, target) >= 0; + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); + return _.map(obj, function(value) { + return (isFunc ? method : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, _.property(key)); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs) { + return _.filter(obj, _.matches(attrs)); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.find(obj, _.matches(attrs)); + }; + + // Return the maximum element (or element-based computation). + _.max = function(obj, iteratee, context) { + var result = -Infinity, lastComputed = -Infinity, + value, computed; + if (iteratee == null && obj != null) { + obj = obj.length === +obj.length ? obj : _.values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value > result) { + result = value; + } + } + } else { + iteratee = _.iteratee(iteratee, context); + _.each(obj, function(value, index, list) { + computed = iteratee(value, index, list); + if (computed > lastComputed || computed === -Infinity && result === -Infinity) { + result = value; + lastComputed = computed; + } + }); + } + return result; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iteratee, context) { + var result = Infinity, lastComputed = Infinity, + value, computed; + if (iteratee == null && obj != null) { + obj = obj.length === +obj.length ? obj : _.values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value < result) { + result = value; + } + } + } else { + iteratee = _.iteratee(iteratee, context); + _.each(obj, function(value, index, list) { + computed = iteratee(value, index, list); + if (computed < lastComputed || computed === Infinity && result === Infinity) { + result = value; + lastComputed = computed; + } + }); + } + return result; + }; + + // Shuffle a collection, using the modern version of the + // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). + _.shuffle = function(obj) { + var set = obj && obj.length === +obj.length ? obj : _.values(obj); + var length = set.length; + var shuffled = Array(length); + for (var index = 0, rand; index < length; index++) { + rand = _.random(0, index); + if (rand !== index) shuffled[index] = shuffled[rand]; + shuffled[rand] = set[index]; + } + return shuffled; + }; + + // Sample **n** random values from a collection. + // If **n** is not specified, returns a single random element. + // The internal `guard` argument allows it to work with `map`. + _.sample = function(obj, n, guard) { + if (n == null || guard) { + if (obj.length !== +obj.length) obj = _.values(obj); + return obj[_.random(obj.length - 1)]; + } + return _.shuffle(obj).slice(0, Math.max(0, n)); + }; + + // Sort the object's values by a criterion produced by an iteratee. + _.sortBy = function(obj, iteratee, context) { + iteratee = _.iteratee(iteratee, context); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value: value, + index: index, + criteria: iteratee(value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(behavior) { + return function(obj, iteratee, context) { + var result = {}; + iteratee = _.iteratee(iteratee, context); + _.each(obj, function(value, index) { + var key = iteratee(value, index, obj); + behavior(result, value, key); + }); + return result; + }; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = group(function(result, value, key) { + if (_.has(result, key)) result[key].push(value); else result[key] = [value]; + }); + + // Indexes the object's values by a criterion, similar to `groupBy`, but for + // when you know that your index values will be unique. + _.indexBy = group(function(result, value, key) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = group(function(result, value, key) { + if (_.has(result, key)) result[key]++; else result[key] = 1; + }); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iteratee, context) { + iteratee = _.iteratee(iteratee, context, 1); + var value = iteratee(obj); + var low = 0, high = array.length; + while (low < high) { + var mid = low + high >>> 1; + if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; + } + return low; + }; + + // Safely create a real, live array from anything iterable. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (obj.length === +obj.length) return _.map(obj, _.identity); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + if (obj == null) return 0; + return obj.length === +obj.length ? obj.length : _.keys(obj).length; + }; + + // Split a collection into two arrays: one whose elements all satisfy the given + // predicate, and one whose elements all do not satisfy the predicate. + _.partition = function(obj, predicate, context) { + predicate = _.iteratee(predicate, context); + var pass = [], fail = []; + _.each(obj, function(value, key, obj) { + (predicate(value, key, obj) ? pass : fail).push(value); + }); + return [pass, fail]; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; + if (n == null || guard) return array[0]; + if (n < 0) return []; + return slice.call(array, 0, n); + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.initial = function(array, n, guard) { + return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + if (array == null) return void 0; + if (n == null || guard) return array[array.length - 1]; + return slice.call(array, Math.max(array.length - n, 0)); + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, n == null || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, _.identity); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, strict, output) { + if (shallow && _.every(input, _.isArray)) { + return concat.apply(output, input); + } + for (var i = 0, length = input.length; i < length; i++) { + var value = input[i]; + if (!_.isArray(value) && !_.isArguments(value)) { + if (!strict) output.push(value); + } else if (shallow) { + push.apply(output, value); + } else { + flatten(value, shallow, strict, output); + } + } + return output; + }; + + // Flatten out an array, either recursively (by default), or just one level. + _.flatten = function(array, shallow) { + return flatten(array, shallow, false, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iteratee, context) { + if (array == null) return []; + if (!_.isBoolean(isSorted)) { + context = iteratee; + iteratee = isSorted; + isSorted = false; + } + if (iteratee != null) iteratee = _.iteratee(iteratee, context); + var result = []; + var seen = []; + for (var i = 0, length = array.length; i < length; i++) { + var value = array[i]; + if (isSorted) { + if (!i || seen !== value) result.push(value); + seen = value; + } else if (iteratee) { + var computed = iteratee(value, i, array); + if (_.indexOf(seen, computed) < 0) { + seen.push(computed); + result.push(value); + } + } else if (_.indexOf(result, value) < 0) { + result.push(value); + } + } + return result; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(flatten(arguments, true, true, [])); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + if (array == null) return []; + var result = []; + var argsLength = arguments.length; + for (var i = 0, length = array.length; i < length; i++) { + var item = array[i]; + if (_.contains(result, item)) continue; + for (var j = 1; j < argsLength; j++) { + if (!_.contains(arguments[j], item)) break; + } + if (j === argsLength) result.push(item); + } + return result; + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = flatten(slice.call(arguments, 1), true, true, []); + return _.filter(array, function(value){ + return !_.contains(rest, value); + }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function(array) { + if (array == null) return []; + var length = _.max(arguments, 'length').length; + var results = Array(length); + for (var i = 0; i < length; i++) { + results[i] = _.pluck(arguments, i); + } + return results; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + if (list == null) return {}; + var result = {}; + for (var i = 0, length = list.length; i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // Return the position of the first occurrence of an item in an array, + // or -1 if the item is not included in the array. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i = 0, length = array.length; + if (isSorted) { + if (typeof isSorted == 'number') { + i = isSorted < 0 ? Math.max(0, length + isSorted) : isSorted; + } else { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + } + for (; i < length; i++) if (array[i] === item) return i; + return -1; + }; + + _.lastIndexOf = function(array, item, from) { + if (array == null) return -1; + var idx = array.length; + if (typeof from == 'number') { + idx = from < 0 ? idx + from + 1 : Math.min(idx, from + 1); + } + while (--idx >= 0) if (array[idx] === item) return idx; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = step || 1; + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var range = Array(length); + + for (var idx = 0; idx < length; idx++, start += step) { + range[idx] = start; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Reusable constructor function for prototype setting. + var Ctor = function(){}; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { + var args, bound; + if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function'); + args = slice.call(arguments, 2); + bound = function() { + if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); + Ctor.prototype = func.prototype; + var self = new Ctor; + Ctor.prototype = null; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (_.isObject(result)) return result; + return self; + }; + return bound; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. _ acts + // as a placeholder, allowing any combination of arguments to be pre-filled. + _.partial = function(func) { + var boundArgs = slice.call(arguments, 1); + return function() { + var position = 0; + var args = boundArgs.slice(); + for (var i = 0, length = args.length; i < length; i++) { + if (args[i] === _) args[i] = arguments[position++]; + } + while (position < arguments.length) args.push(arguments[position++]); + return func.apply(this, args); + }; + }; + + // Bind a number of an object's methods to that object. Remaining arguments + // are the method names to be bound. Useful for ensuring that all callbacks + // defined on an object belong to it. + _.bindAll = function(obj) { + var i, length = arguments.length, key; + if (length <= 1) throw new Error('bindAll must be passed function names'); + for (i = 1; i < length; i++) { + key = arguments[i]; + obj[key] = _.bind(obj[key], obj); + } + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memoize = function(key) { + var cache = memoize.cache; + var address = hasher ? hasher.apply(this, arguments) : key; + if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); + return cache[address]; + }; + memoize.cache = {}; + return memoize; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ + return func.apply(null, args); + }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + _.throttle = function(func, wait, options) { + var context, args, result; + var timeout = null; + var previous = 0; + if (!options) options = {}; + var later = function() { + previous = options.leading === false ? 0 : _.now(); + timeout = null; + result = func.apply(context, args); + if (!timeout) context = args = null; + }; + return function() { + var now = _.now(); + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0 || remaining > wait) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + if (!timeout) context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, args, context, timestamp, result; + + var later = function() { + var last = _.now() - timestamp; + + if (last < wait && last > 0) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + if (!timeout) context = args = null; + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = _.now(); + var callNow = immediate && !timeout; + if (!timeout) timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + + return result; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return _.partial(wrapper, func); + }; + + // Returns a negated version of the passed-in predicate. + _.negate = function(predicate) { + return function() { + return !predicate.apply(this, arguments); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var args = arguments; + var start = args.length - 1; + return function() { + var i = start; + var result = args[start].apply(this, arguments); + while (i--) result = args[i].call(this, result); + return result; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Returns a function that will only be executed before being called N times. + _.before = function(times, func) { + var memo; + return function() { + if (--times > 0) { + memo = func.apply(this, arguments); + } else { + func = null; + } + return memo; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = _.partial(_.before, 2); + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = function(obj) { + if (!_.isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys.push(key); + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var values = Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[keys[i]]; + } + return values; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var pairs = Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [keys[i], obj[keys[i]]]; + } + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + result[obj[keys[i]]] = keys[i]; + } + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + if (!_.isObject(obj)) return obj; + var source, prop; + for (var i = 1, length = arguments.length; i < length; i++) { + source = arguments[i]; + for (prop in source) { + if (hasOwnProperty.call(source, prop)) { + obj[prop] = source[prop]; + } + } + } + return obj; + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(obj, iteratee, context) { + var result = {}, key; + if (obj == null) return result; + if (_.isFunction(iteratee)) { + iteratee = createCallback(iteratee, context); + for (key in obj) { + var value = obj[key]; + if (iteratee(value, key, obj)) result[key] = value; + } + } else { + var keys = concat.apply([], slice.call(arguments, 1)); + obj = new Object(obj); + for (var i = 0, length = keys.length; i < length; i++) { + key = keys[i]; + if (key in obj) result[key] = obj[key]; + } + } + return result; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj, iteratee, context) { + if (_.isFunction(iteratee)) { + iteratee = _.negate(iteratee); + } else { + var keys = _.map(concat.apply([], slice.call(arguments, 1)), String); + iteratee = function(value, key) { + return !_.contains(keys, key); + }; + } + return _.pick(obj, iteratee, context); + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + if (!_.isObject(obj)) return obj; + for (var i = 1, length = arguments.length; i < length; i++) { + var source = arguments[i]; + for (var prop in source) { + if (obj[prop] === void 0) obj[prop] = source[prop]; + } + } + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a === 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className !== toString.call(b)) return false; + switch (className) { + // Strings, numbers, regular expressions, dates, and booleans are compared by value. + case '[object RegExp]': + // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return '' + a === '' + b; + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. + // Object(NaN) is equivalent to NaN + if (+a !== +a) return +b !== +b; + // An `egal` comparison is performed for other numeric values. + return +a === 0 ? 1 / +a === 1 / b : +a === +b; + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a === +b; + } + if (typeof a != 'object' || typeof b != 'object') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] === a) return bStack[length] === b; + } + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if ( + aCtor !== bCtor && + // Handle Object.create(x) cases + 'constructor' in a && 'constructor' in b && + !(_.isFunction(aCtor) && aCtor instanceof aCtor && + _.isFunction(bCtor) && bCtor instanceof bCtor) + ) { + return false; + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + var size, result; + // Recursively compare objects and arrays. + if (className === '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size === b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + if (!(result = eq(a[size], b[size], aStack, bStack))) break; + } + } + } else { + // Deep compare objects. + var keys = _.keys(a), key; + size = keys.length; + // Ensure that both objects contain the same number of properties before comparing deep equality. + result = _.keys(b).length === size; + if (result) { + while (size--) { + // Deep compare each member + key = keys[size]; + if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; + } + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return result; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, [], []); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (_.isArray(obj) || _.isString(obj) || _.isArguments(obj)) return obj.length === 0; + for (var key in obj) if (_.has(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) === '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + var type = typeof obj; + return type === 'function' || type === 'object' && !!obj; + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. + _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) === '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return _.has(obj, 'callee'); + }; + } + + // Optimize `isFunction` if appropriate. Work around an IE 11 bug. + if (typeof /./ !== 'function') { + _.isFunction = function(obj) { + return typeof obj == 'function' || false; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return isFinite(obj) && !isNaN(parseFloat(obj)); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj !== +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return obj != null && hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iteratees. + _.identity = function(value) { + return value; + }; + + _.constant = function(value) { + return function() { + return value; + }; + }; + + _.noop = function(){}; + + _.property = function(key) { + return function(obj) { + return obj[key]; + }; + }; + + // Returns a predicate for checking whether an object has a given set of `key:value` pairs. + _.matches = function(attrs) { + var pairs = _.pairs(attrs), length = pairs.length; + return function(obj) { + if (obj == null) return !length; + obj = new Object(obj); + for (var i = 0; i < length; i++) { + var pair = pairs[i], key = pair[0]; + if (pair[1] !== obj[key] || !(key in obj)) return false; + } + return true; + }; + }; + + // Run a function **n** times. + _.times = function(n, iteratee, context) { + var accum = Array(Math.max(0, n)); + iteratee = createCallback(iteratee, context, 1); + for (var i = 0; i < n; i++) accum[i] = iteratee(i); + return accum; + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + }; + + // A (possibly faster) way to get the current timestamp as an integer. + _.now = Date.now || function() { + return new Date().getTime(); + }; + + // List of HTML entities for escaping. + var escapeMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }; + var unescapeMap = _.invert(escapeMap); + + // Functions for escaping and unescaping strings to/from HTML interpolation. + var createEscaper = function(map) { + var escaper = function(match) { + return map[match]; + }; + // Regexes for identifying a key that needs to be escaped + var source = '(?:' + _.keys(map).join('|') + ')'; + var testRegexp = RegExp(source); + var replaceRegexp = RegExp(source, 'g'); + return function(string) { + string = string == null ? '' : '' + string; + return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; + }; + }; + _.escape = createEscaper(escapeMap); + _.unescape = createEscaper(unescapeMap); + + // If the value of the named `property` is a function then invoke it with the + // `object` as context; otherwise, return it. + _.result = function(object, property) { + if (object == null) return void 0; + var value = object[property]; + return _.isFunction(value) ? object[property]() : value; + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\u2028|\u2029/g; + + var escapeChar = function(match) { + return '\\' + escapes[match]; + }; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + // NB: `oldSettings` only exists for backwards compatibility. + _.template = function(text, settings, oldSettings) { + if (!settings && oldSettings) settings = oldSettings; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset).replace(escaper, escapeChar); + index = offset + match.length; + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } else if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } else if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + + // Adobe VMs need the match returned to produce the correct offest. + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + 'return __p;\n'; + + try { + var render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled source as a convenience for precompilation. + var argument = settings.variable || 'obj'; + template.source = 'function(' + argument + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function. Start chaining a wrapped Underscore object. + _.chain = function(obj) { + var instance = _(obj); + instance._chain = true; + return instance; + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(obj) { + return this._chain ? _(obj).chain() : obj; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + _.each(_.functions(obj), function(name) { + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result.call(this, func.apply(_, args)); + }; + }); + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; + return result.call(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + _.each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result.call(this, method.apply(this._wrapped, arguments)); + }; + }); + + // Extracts the result from a wrapped and chained object. + _.prototype.value = function() { + return this._wrapped; + }; + + // AMD registration happens at the end for compatibility with AMD loaders + // that may not enforce next-turn semantics on modules. Even though general + // practice for AMD registration is to be anonymous, underscore registers + // as a named module because, like jQuery, it is a base library that is + // popular enough to be bundled in a third party lib, but not be part of + // an AMD load request. Those cases could generate an error when an + // anonymous define() is called outside of a loader request. + if (typeof define === 'function' && define.amd) { + define('underscore', [], function() { + return _; + }); + } +}.call(this)); diff --git a/webapp/app/index.html b/webapp/app/index.html index 8b90b8c86..e48e5b606 100644 --- a/webapp/app/index.html +++ b/webapp/app/index.html @@ -52,6 +52,7 @@ + @@ -77,6 +78,10 @@ + + + + @@ -114,6 +119,8 @@ + + diff --git a/webapp/app/js/app.js b/webapp/app/js/app.js index 0aa7c2168..b1bdac976 100644 --- a/webapp/app/js/app.js +++ b/webapp/app/js/app.js @@ -1,2 +1,2 @@ //Kylin Application Module -KylinApp = angular.module('kylin', ['ngRoute', 'ngResource', 'ngGrid', 'ui.bootstrap', 'ui.ace', 'base64', 'angularLocalStorage', 'localytics.directives', 'treeControl', 'nvd3ChartDirectives','ngLoadingRequest','oitozero.ngSweetAlert','ngCookies']); \ No newline at end of file +KylinApp = angular.module('kylin', ['ngRoute', 'ngResource', 'ngGrid', 'ui.bootstrap', 'ui.ace', 'base64', 'angularLocalStorage', 'localytics.directives', 'treeControl', 'nvd3ChartDirectives','ngLoadingRequest','oitozero.ngSweetAlert','ngCookies','angular-underscore', 'ngAnimate', 'ui.sortable']); diff --git a/webapp/app/js/controllers/auth.js b/webapp/app/js/controllers/auth.js index 81c7db2ab..4976db69a 100644 --- a/webapp/app/js/controllers/auth.js +++ b/webapp/app/js/controllers/auth.js @@ -8,9 +8,7 @@ KylinApp.controller('LoginCtrl', function ($scope, $rootScope, $location, $base6 $scope.login = function () { // set the basic authentication header that will be parsed in the next request and used to authenticate httpHeaders.common['Authorization'] = 'Basic ' + $base64.encode($scope.username + ':' + $scope.password); - $scope.loading = true; - //verify project if($scope.project.projects.length&&!$scope.project.selectedProject){ $scope.loading = false; diff --git a/webapp/app/js/controllers/cubeDimensions.js b/webapp/app/js/controllers/cubeDimensions.js new file mode 100644 index 000000000..24283bbe2 --- /dev/null +++ b/webapp/app/js/controllers/cubeDimensions.js @@ -0,0 +1,435 @@ +'use strict'; + +KylinApp.controller('CubeDimensionsCtrl', function ($scope, $modal) { + // Available columns list derived from cube data model. + $scope.availableColumns = {}; + + // Columns selected and disabled status bound to UI, group by table. + $scope.selectedColumns = {}; + + // Available tables cache: 1st is the fact table, next are lookup tables. + $scope.availableTables = []; + + // Do some cube dimensions adaption between new and old cube schema. TODO new cube schema change. + $scope.prepareDimensions = function (dimensions) { + angular.forEach(dimensions, function (dim) { + // Flatten hierarchy array by stripping level replacing with array index. + if (dim.hierarchy && dim.hierarchy.length) { + var flatten = []; + + angular.forEach(dim.hierarchy, function (value) { + flatten.push(value.column); + }); + + dim.hierarchy = flatten; + } + }); + }; + + $scope.prepareDimensions($scope.cubeMetaFrame.dimensions); + + // Helper func to get join info from cube data model. + var getJoin = function (tableName) { + var join = null; + + for (var j = 0; j < $scope.cubeMetaFrame.model.lookups.length; j++) { + if ($scope.cubeMetaFrame.model.lookups[j].table == tableName) { + join = $scope.cubeMetaFrame.model.lookups[j].join; + break; + } + } + + return join; + }; + + /** + * Helper func to get columns that dimensions based on, three cases: + * 1. normal dimension: column array. + * 2. hierarchy dimension: column array, the array index is the hierarchy level. + * 3. derived dimension: derived columns array. + * TODO new cube schema change + */ + var dimCols = function (dim) { + var referredCols = []; + + // Case 3. + if (dim.derived && dim.derived.length) { + referredCols = referredCols.concat(dim.derived); + } + + // Case 2. + if (dim.hierarchy && dim.hierarchy.length) { + referredCols = referredCols.concat(dim.hierarchy); + } + + // Case 1. + if (!dim.derived && !dim.hierarchy) { + referredCols.push(dim.column); + } + + return referredCols; + }; + + // Dump available columns plus column table name, whether is from lookup table. + $scope.initColumns = function () { + var factTable = $scope.cubeMetaFrame.model.fact_table; + + // At first dump the columns of fact table. + var cols = $scope.getColumnsByTable(factTable); + + // Initialize selected available. + var factAvailable = {}; + var factSelectAvailable = {}; + + for (var i = 0; i < cols.length; i++) { + cols[i].table = factTable; + cols[i].isLookup = false; + + factAvailable[cols[i].name] = cols[i]; + + // Default not selected and not disabled. + factSelectAvailable[cols[i].name] = {selected: false, disabled: false}; + } + + $scope.availableColumns[factTable] = factAvailable; + $scope.selectedColumns[factTable] = factSelectAvailable; + $scope.availableTables.push(factTable); + + // Then dump each lookup tables. + var lookups = $scope.cubeMetaFrame.model.lookups; + + for (var j = 0; j < lookups.length; j++) { + var cols2 = $scope.getColumnsByTable(lookups[j].table); + + // Initialize selected available. + var lookupAvailable = {}; + var lookupSelectAvailable = {}; + + for (var k = 0; k < cols2.length; k++) { + cols2[k].table = lookups[j].table; + cols2[k].isLookup = true; + + lookupAvailable[cols2[k].name] = cols2[k]; + + // Default not selected and not disabled. + lookupSelectAvailable[cols2[k].name] = {selected: false, disabled: false}; + } + + $scope.availableColumns[lookups[j].table] = lookupAvailable; + $scope.selectedColumns[lookups[j].table] = lookupSelectAvailable; + $scope.availableTables.push(lookups[j].table); + } + }; + + // Check column status: selected or disabled based on current cube dimensions. + $scope.initColumnStatus = function () { + angular.forEach($scope.cubeMetaFrame.dimensions, function (dim) { + var cols = dimCols(dim); + + angular.forEach(cols, function (colName) { + $scope.selectedColumns[dim.table][colName] = {selected: true, disabled: true}; + }); + }); + }; + + // Initialize data for columns widget in auto-gen when add/edit cube. + if ($scope.state.mode == 'edit') { + $scope.initColumns(); + } + + // Initialize params for add/edit dimension. + $scope.dimState = { + editing: false, + editingIndex: -1, + filter: '' + }; + + // Init the dimension, dimension name default as the column key. TODO new cube schema change. + var Dimension = function (table, selectedCols, dimType) { + var origin = {name: '', table: table}; + + switch (dimType) { + case 'normal': + // Default name as 1st column name. + if (table && selectedCols.length) { + origin.name = table + '.' + selectedCols[0]; + } + + origin.column = selectedCols[0]; + break; + + case 'derived': + if (table && selectedCols.length) { + origin.name = table + '_derived'; + } + + origin.column = '{FK}'; + origin.derived = selectedCols; + break; + + case 'hierarchy': + if (table && selectedCols.length) { + origin.name = table + '_hierarchy'; + } + + origin.hierarchy = selectedCols; + break; + } + + return origin; + }; + + // Since old schema may be both derived and hierarchy. TODO new cube schema change. + $scope.getDimType = function (dim) { + var types = []; + + if (dim.derived && dim.derived.length) { + types.push('derived'); + } + + if (dim.hierarchy && dim.hierarchy.length) { + types.push('hierarchy'); + } + + if (!types.length) { + types.push('normal'); + } + + return types; + }; + + var dimList = $scope.cubeMetaFrame.dimensions; + + // Open add/edit dimension modal. + $scope.openDimModal = function (dimType) { + var modalInstance = $modal.open({ + templateUrl: 'addEditDimension.html', + controller: cubeDimModalCtrl, + backdrop: 'static', + scope: $scope, + resolve: { + dimType: function () { + // For old schema compatibility, convert into array here. TODO new cube schema change. + return angular.isArray(dimType) ? dimType : [dimType]; + } + } + }); + + modalInstance.result.then(function () { + if (!$scope.dimState.editing) { + $scope.doneAddDim(); + } else { + $scope.doneEditDim(); + } + + }, function () { + $scope.cancelDim(); + }); + }; + + // Controller for cube dimension add/edit modal. + var cubeDimModalCtrl = function ($scope, $modalInstance, dimType) { + $scope.dimType = dimType; + + $scope.ok = function () { + $modalInstance.close(); + }; + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + }; + + $scope.addDim = function (dimType) { + $scope.newDimension = Dimension('', [], dimType); + + $scope.openDimModal(dimType); + }; + + $scope.editDim = function (dim) { + $scope.dimState.editingIndex = dimList.indexOf(dim); + $scope.dimState.editing = true; + + // Make a copy of model will be editing. + $scope.newDimension = angular.copy(dim); + + $scope.openDimModal($scope.getDimType(dim)); + }; + + $scope.doneAddDim = function () { + // Push new dimension which bound user input data. + dimList.push(angular.copy($scope.newDimension)); + + $scope.resetParams(); + }; + + $scope.doneEditDim = function () { + // Copy edited model to destination model. + angular.copy($scope.newDimension, dimList[$scope.dimState.editingIndex]); + + $scope.resetParams(); + }; + + $scope.cancelDim = function () { + $scope.resetParams(); + }; + + $scope.removeDim = function (dim) { + dimList.splice(dimList.indexOf(dim), 1); + }; + + $scope.resetParams = function () { + $scope.dimState.editing = false; + $scope.dimState.editingIndex = -1; + + $scope.newDimension = {}; + }; + + // Open auto-gen dimension modal. + $scope.openAutoGenModal = function (dimType) { + // Init columns status. + $scope.initColumnStatus(); + + var modalInstance = $modal.open({ + templateUrl: 'autoGenDimension.html', + controller: cubeAutoGenDimModalCtrl, + backdrop: 'static', + scope: $scope + }); + + modalInstance.result.then(function () { + $scope.autoGenDims(); + }, function () { + $scope.resetGenDims(); + }); + }; + + // Controller for cube dimension auto-gen modal. + var cubeAutoGenDimModalCtrl = function ($scope, $modalInstance) { + $scope.ok = function () { + $modalInstance.close(); + }; + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + }; + + // Helper func to get the selected status in auto gen. + $scope.getSelectedCols = function () { + var selectedCols = {}; + + angular.forEach($scope.selectedColumns, function (value, table) { + angular.forEach(value, function (status, colName) { + if (status.selected && !status.disabled) { + if (!selectedCols[table]) { + selectedCols[table] = []; + } + + selectedCols[table].push(colName); + } + }); + }); + + return selectedCols; + }; + + // Auto generate dimensions. + $scope.autoGenDims = function () { + var selectedCols = $scope.getSelectedCols(); + + angular.forEach(selectedCols, function (cols, table) { + if ($scope.cubeMetaFrame.model.fact_table == table) { + // Fact table: for each selected column, create one normal dimension. + for (var i = 0; i < cols.length; i++) { + dimList.push(Dimension(table, [cols[i]], 'normal')); + } + } else { + // Per lookup table, create one derived dimension for all its selected columns; + if (cols.length) { + dimList.push(Dimension(table, cols, 'derived')); + } + } + }); + }; + + // Just reset the selected status of columns. + $scope.resetGenDims = function () { + var selectedCols = $scope.getSelectedCols(); + + angular.forEach(selectedCols, function (cols, table) { + for (var i = 0; i < cols.length; i++) { + $scope.selectedColumns[table][cols[i]].selected = false; + } + }); + }; + + // Check whether there is column conflicts. + $scope.dimConflicts = []; + + $scope.$watch('cubeMetaFrame.dimensions', function (newVal, oldVal) { + if (!newVal || !newVal.length) { + return; + } + + var referredCols = {}; + + angular.forEach(newVal, function (curDim) { + var table = curDim.table; + var cols = dimCols(curDim); + + for (var i = 0; i < cols.length; i++) { + var key = table + '.' + cols[i]; + + if (!referredCols[key]) { + referredCols[key] = []; + } + + referredCols[key].push({id: curDim.id, name: curDim.name}); + } + }); + + var conflicts = []; + + angular.forEach(referredCols, function (dims, key) { + if (dims.length > 1) { + // More than 1 dimensions has referred this column. + var colInfo = key.split('.'); + conflicts.push({table: colInfo[0], column: colInfo[1], dims: dims}); + } + }); + + $scope.dimConflicts = conflicts; + }, true); + + + // Adapter between new data model/dimensions and original dimensions. + $scope.dimensionsAdapter = function () { + angular.forEach($scope.cubeMetaFrame.dimensions, function (dim) { + // Lookup table column, add 'join' info. TODO new cube schema change. + if (dim.derived && dim.derived.length) { + dim.join = getJoin(dim.table); + } + + // Hierarchy level. TODO new cube schema change. + if (dim.hierarchy && dim.hierarchy.length) { + var h2 = []; + + angular.forEach(dim.hierarchy, function (value, index) { + h2.push({level: index + 1, column: value}); + }); + + dim.hierarchy = h2; + } + }); + }; + + if ($scope.state.mode == 'edit') { + $scope.$on('$destroy', function () { + $scope.dimensionsAdapter(); + + // Emit dimensions edit event in order to re-generate row key. + $scope.$emit('DimensionsEdited'); + }); + } +}); diff --git a/webapp/app/js/controllers/cubeEdit.js b/webapp/app/js/controllers/cubeEdit.js index 47f62fc3c..94e92dae3 100644 --- a/webapp/app/js/controllers/cubeEdit.js +++ b/webapp/app/js/controllers/cubeEdit.js @@ -6,10 +6,7 @@ KylinApp.controller('CubeEditCtrl', function ($scope, $q, $routeParams, $locatio //add or edit ? var absUrl = $location.absUrl(); $scope.cubeMode = absUrl.indexOf("/cubes/add")!=-1?'addNewCube':absUrl.indexOf("/cubes/edit")!=-1?'editExistCube':'default'; - // use this flag to listen when rm or add dimension edited,used in sub-controller cube-schema - $scope.editFlag ={ - dimensionEdited:"init" - }; + //~ Define metadata & class $scope.measureParamType = ['column', 'constant']; $scope.measureExpressions = ['SUM', 'MIN', 'MAX', 'COUNT', 'COUNT_DISTINCT']; @@ -95,9 +92,9 @@ KylinApp.controller('CubeEditCtrl', function ($scope, $q, $routeParams, $locatio "cube_partition_desc": { "partition_date_column": null, "partition_date_start": null, - "cube_partition_type": null + "cube_partition_type": 'APPEND' }, - "capacity": "", + "capacity": "MEDIUM", "cost": 50, "dimensions": [], "measures": [ @@ -219,13 +216,9 @@ KylinApp.controller('CubeEditCtrl', function ($scope, $q, $routeParams, $locatio } } else { $scope.cubeMetaFrame.project = $scope.state.project; - if(request.message){ var message =request.message; var msg = !!(message) ? message : 'Failed to take action.'; - MessageService.sendMsg($scope.cubeResultTmpl({'text':'Failed to update the cube.','schema':$scope.state.cubeSchema}), 'error', {}, true, 'top_center'); - }else{ - SweetAlert.swal('Oops...', "Failed to take action.", 'error'); - } + MessageService.sendMsg($scope.cubeResultTmpl({'text':msg,'schema':$scope.state.cubeSchema}), 'error', {}, true, 'top_center'); } //end loading loadingRequest.hide(); @@ -249,13 +242,9 @@ KylinApp.controller('CubeEditCtrl', function ($scope, $q, $routeParams, $locatio MessageService.sendMsg($scope.cubeResultTmpl({'text':'Created the cube successfully.',type:'success'}), 'success', {}, true, 'top_center'); } else { $scope.cubeMetaFrame.project = $scope.state.project; - if(request.message){ - var message =request.message; - var msg = !!(message) ? message : 'Failed to take action.'; - MessageService.sendMsg($scope.cubeResultTmpl({'text':msg,'schema':$scope.state.cubeSchema}), 'error', {}, true, 'top_center'); - } else { - MessageService.sendMsg($scope.cubeResultTmpl({'text':"Failed to take action.",'schema':$scope.state.cubeSchema}), 'error', {}, true, 'top_center'); - } + var message =request.message; + var msg = !!(message) ? message : 'Failed to take action.'; + MessageService.sendMsg($scope.cubeResultTmpl({'text':msg,'schema':$scope.state.cubeSchema}), 'error', {}, true, 'top_center'); } //end loading @@ -461,7 +450,7 @@ KylinApp.controller('CubeEditCtrl', function ($scope, $q, $routeParams, $locatio function sliceGroupItemToGroups(groupItems){ if(!groupItems.length){ - return; + return []; } var groups = []; var j = -1; @@ -542,12 +531,8 @@ KylinApp.controller('CubeEditCtrl', function ($scope, $q, $routeParams, $locatio } }); - - $scope.$watchCollection('editFlag.dimensionEdited', function (newValue, oldValue) { - if(newValue=="init"){ - return; - } - if($scope.cubeMetaFrame){ + $scope.$on('DimensionsEdited', function (event) { + if ($scope.cubeMetaFrame) { reGenerateRowKey(); } }); diff --git a/webapp/app/js/controllers/cubeModel.js b/webapp/app/js/controllers/cubeModel.js new file mode 100644 index 000000000..be3e32a66 --- /dev/null +++ b/webapp/app/js/controllers/cubeModel.js @@ -0,0 +1,145 @@ +'use strict'; + +KylinApp.controller('CubeModelCtrl', function ($scope, $modal) { + var DataModel = function () { + return { + name: '', + fact_table: '', + lookups: [] + }; + }; + + // Adapter between new data model and legacy cube schema. + $scope.prepareModel = function () { + if (!$scope.cubeMetaFrame.hasOwnProperty('model')) { + // Old version cube schema does not have model concept, try to build one based on legacy schema. + if ($scope.cubeMetaFrame.fact_table) { + // This is the case when editing cube. + var model = DataModel(); + + model.fact_table = $scope.cubeMetaFrame.fact_table; + + // Get join relationships for old dimensions which using join. + var tables = []; + + angular.forEach($scope.cubeMetaFrame.dimensions, function (dim) { + // De-duplicate: adopt 1st one. + if (tables.indexOf(dim.table) == -1 && dim.join && dim.join.primary_key.length) { + tables.push(dim.table); + model.lookups.push({table: dim.table, join: dim.join}); + } + }); + + $scope.cubeMetaFrame.model = model; + } else { + // This is the case when create new cube. + $scope.cubeMetaFrame.model = DataModel(); + } + + // Currently set model name same as cube name, hidden from user. + $scope.cubeMetaFrame.model.name = $scope.cubeMetaFrame.name; + } + }; + + // TODO this is for legacy cube schema. + $scope.prepareModel(); + + var Lookup = function () { + return { + table: '', + join: { + type: '', + primary_key: [], + foreign_key: [] + } + }; + }; + + // Initialize params. + $scope.lookupState = { + editing: false, + editingIndex: -1, + filter: '' + }; + + $scope.newLookup = Lookup(); + + var lookupList = $scope.cubeMetaFrame.model.lookups; + + $scope.openLookupModal = function () { + var modalInstance = $modal.open({ + templateUrl: 'dataModelLookupTable.html', + controller: cubeModelLookupModalCtrl, + backdrop: 'static', + scope: $scope + }); + + modalInstance.result.then(function () { + if (!$scope.lookupState.editing) { + $scope.doneAddLookup(); + } else { + $scope.doneEditLookup(); + } + + }, function () { + $scope.cancelLookup(); + }); + }; + + // Controller for cube model lookup modal. + var cubeModelLookupModalCtrl = function ($scope, $modalInstance) { + $scope.ok = function () { + $modalInstance.close(); + }; + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + }; + + $scope.editLookup = function (lookup) { + $scope.lookupState.editingIndex = lookupList.indexOf(lookup); + $scope.lookupState.editing = true; + + // Make a copy of model will be editing. + $scope.newLookup = angular.copy(lookup); + + $scope.openLookupModal(); + }; + + $scope.doneAddLookup = function () { + // Push newLookup which bound user input data. + lookupList.push(angular.copy($scope.newLookup)); + + $scope.resetParams(); + }; + + $scope.doneEditLookup = function () { + // Copy edited model to destination model. + angular.copy($scope.newLookup, lookupList[$scope.lookupState.editingIndex]); + + $scope.resetParams(); + }; + + $scope.cancelLookup = function () { + $scope.resetParams(); + }; + + $scope.removeLookup = function (lookup) { + lookupList.splice(lookupList.indexOf(lookup), 1); + }; + + $scope.resetParams = function () { + $scope.lookupState.editing = false; + $scope.lookupState.editingIndex = -1; + + $scope.newLookup = Lookup(); + }; + + // This is for legacy compatibility, assign 'fact_table' property. TODO new cube schema change. + $scope.$on('$destroy', function () { + if (!$scope.cubeMetaFrame.fact_table) { + $scope.cubeMetaFrame.fact_table = $scope.cubeMetaFrame.model.fact_table; + } + }); +}); diff --git a/webapp/app/js/controllers/cubeSchema.js b/webapp/app/js/controllers/cubeSchema.js index df0f6444a..27c0523bc 100644 --- a/webapp/app/js/controllers/cubeSchema.js +++ b/webapp/app/js/controllers/cubeSchema.js @@ -10,6 +10,7 @@ KylinApp.controller('CubeSchemaCtrl', function ($scope, QueryService, UserServic $scope.wizardSteps = [ {title: 'Cube Info', src: 'partials/cubeDesigner/info.html', isComplete: false}, + {title: 'Data Model', src: 'partials/cubeDesigner/data_model.html', isComplete: false}, {title: 'Dimensions', src: 'partials/cubeDesigner/dimensions.html', isComplete: false}, {title: 'Measures', src: 'partials/cubeDesigner/measures.html', isComplete: false}, {title: 'Filter', src: 'partials/cubeDesigner/filter.html', isComplete: false}, @@ -22,33 +23,6 @@ KylinApp.controller('CubeSchemaCtrl', function ($scope, QueryService, UserServic $scope.curStep = $scope.wizardSteps[0]; - var Dimension = { - createNew: function () { - var dimension = { - "id": "", - "name": "", - "table": "", - "column": "", - "datatype": "", - "derived": [], - "join": { - "type": "", - "primary_key": [], - "foreign_key": [] - }, - "hierarchy": [], - "status": { - "joinCount": 1, - "hierarchyCount": 1, - "useHierarchy": false, - "useJoin": false - } - }; - - return dimension; - } - }; - var Measure = { createNew: function () { var measure = { @@ -82,56 +56,28 @@ KylinApp.controller('CubeSchemaCtrl', function ($scope, QueryService, UserServic $scope.$watch('cubeMetaFrame', function (newValue, oldValue) { if ($scope.cubeMode=="editExistCube"&&newValue && !newValue.project) { initProject(); - generateCubeStatus($scope.cubeMetaFrame); } }); // ~ public methods $scope.filterProj = function(project){ return $scope.userService.hasRole('ROLE_ADMIN') || $scope.hasPermission(project,$scope.permissions.ADMINISTRATION.mask); - } - - $scope.addNewDimension = function (dimension) { - $scope.newDimension = (!!dimension)? dimension: Dimension.createNew(); - if(!$scope.newDimension.join){ - $scope.newDimension.join = { "type": "","primary_key": [],"foreign_key": []} - } - if($scope.newDimension.status&&$scope.newDimension.status.useJoin||$scope.newDimension.join.foreign_key.length!=0){ - $scope.newDimension.status.useJoin = true; - } - } - - $scope.clearNewDimension = function () { - $scope.newDimension = null; - } - - $scope.saveNewDimension = function () { - if($scope.editFlag.dimensionEdited=="init"){ - $scope.editFlag.dimensionEdited = false; - }else{ - $scope.editFlag.dimensionEdited=!$scope.editFlag.dimensionEdited; - } - - if ($scope.cubeMetaFrame.dimensions.indexOf($scope.newDimension) === -1) { - $scope.cubeMetaFrame.dimensions.push($scope.newDimension); - } - $scope.newDimension = null; - } + }; $scope.addNewMeasure = function (measure) { $scope.newMeasure = (!!measure)? measure:Measure.createNew(); - } + }; $scope.clearNewMeasure = function () { $scope.newMeasure = null; - } + }; $scope.saveNewMeasure = function () { if ($scope.cubeMetaFrame.measures.indexOf($scope.newMeasure) === -1) { $scope.cubeMetaFrame.measures.push($scope.newMeasure); } $scope.newMeasure = null; - } + }; $scope.addNewRowkeyColumn = function () { $scope.cubeMetaFrame.rowkey.rowkey_columns.push({ @@ -140,74 +86,24 @@ KylinApp.controller('CubeSchemaCtrl', function ($scope, QueryService, UserServic "dictionary": "true", "mandatory": false }); - } + }; $scope.addNewAggregationGroup = function () { $scope.cubeMetaFrame.rowkey.aggregation_groups.push([]); - } + }; $scope.refreshAggregationGroup = function (list, index, aggregation_groups) { if (aggregation_groups) { list[index] = aggregation_groups; } - } - - $scope.addNewHierarchy = function (dimension) { - if (!dimension.hierarchy) { - dimension.hierarchy = []; - } - dimension.hierarchy.push({ - "level": (dimension.hierarchy.length + 1), - "column": undefined - }); - } - - $scope.addNewDerived = function (dimension) { - if(!dimension.derived){ - dimension.derived = []; - } - dimension.derived.push(""); - } - - $scope.toggleJoin = function (dimension,$event) { - if (dimension.join&&dimension.join.type!='') { - if(!confirm('Delete the join relation?')){ - $event.preventDefault(); - return; - }else{ - delete dimension.join; - } - } - else { - dimension.join = dimension.join==undefined?{}:dimension.join; - dimension.join.type = 'left'; - } - } - - $scope.toggleHierarchy = function (dimension) { - if (dimension.status.useHierarchy) { - dimension.hierarchy = []; - } - } + }; $scope.removeElement = function (arr, element) { var index = arr.indexOf(element); if (index > -1) { arr.splice(index, 1); } - } - - $scope.removeDimension = function (arr, element) { - var index = arr.indexOf(element); - if (index > -1) { - arr.splice(index, 1); - if($scope.editFlag.dimensionEdited=="init"){ - $scope.editFlag.dimensionEdited = false; - }else{ - $scope.editFlag.dimensionEdited=!$scope.editFlag.dimensionEdited; - } - } - } + }; $scope.open = function ($event) { $event.preventDefault(); @@ -222,7 +118,7 @@ KylinApp.controller('CubeSchemaCtrl', function ($scope, QueryService, UserServic $scope.curStep.isComplete = false; $scope.curStep = $scope.wizardSteps[stepIndex - 1]; } - } + }; $scope.nextView = function () { var stepIndex = $scope.wizardSteps.indexOf($scope.curStep); @@ -234,19 +130,7 @@ KylinApp.controller('CubeSchemaCtrl', function ($scope, QueryService, UserServic UserService.setCurUser(data); }); } - } - - $scope.getJoinToolTip = function (dimension) { - var tooltip = ""; - - if (dimension.join) { - angular.forEach(dimension.join.primary_key, function (pk, index) { - tooltip += (pk + " = " + dimension.join.foreign_key[index] + "
"); - }); - } - - return tooltip; - } + }; // ~ private methods function initProject() { @@ -272,15 +156,4 @@ KylinApp.controller('CubeSchemaCtrl', function ($scope, QueryService, UserServic }); }); } - - function generateCubeStatus(cubeMeta) { - angular.forEach(cubeMeta.dimensions, function (dimension, index) { - dimension.status = {}; - if (dimension.hierarchy) { - dimension.status.useHierarchy = true; - dimension.status.joinCount = (!!dimension.join.primary_key) ? dimension.join.primary_key.length : 0; - dimension.status.hierarchyCount = (!!dimension.hierarchy) ? dimension.hierarchy.length : 0; - } - }); - } }); diff --git a/webapp/app/js/controllers/cubes.js b/webapp/app/js/controllers/cubes.js index b544385f3..593012157 100644 --- a/webapp/app/js/controllers/cubes.js +++ b/webapp/app/js/controllers/cubes.js @@ -1,7 +1,7 @@ 'use strict'; KylinApp - .controller('CubesCtrl', function ($scope, $q, $routeParams, $location, $modal, MessageService, CubeDescService, CubeService, JobService, UserService, ProjectService,SweetAlert,loadingRequest) { + .controller('CubesCtrl', function ($scope, $q, $routeParams, $location, $modal, MessageService, CubeDescService, CubeService, JobService, UserService, ProjectService,SweetAlert,loadingRequest,$log) { $scope.listParams={ cubeName: $routeParams.cubeName, projectName: $routeParams.projectName @@ -27,8 +27,8 @@ KylinApp dimensionFilter: '', measureFilter: ''}; $scope.list = function (offset, limit) { - if(!$scope.project.selectedProject){ - return; + if(!$scope.project.projects.length){ + return []; } offset = (!!offset) ? offset : 0; limit = (!!limit) ? limit : 20; @@ -38,11 +38,7 @@ KylinApp if ($scope.listParams.cubeName) { queryParam.cubeName = $scope.listParams.cubeName; } - if ($scope.project.selectedProject){ - queryParam.projectName = $scope.project.selectedProject; - }else{ - queryParam.projectName = $scope.project.projects[0]; - } + queryParam.projectName = $scope.project.selectedProject; $scope.loading = true; CubeService.list(queryParam, function (cubes) { @@ -65,7 +61,6 @@ KylinApp $scope.loadDetail(cube); } }); - $scope.cubes=[]; $scope.cubes = $scope.cubes.concat(cubes); $scope.loading = false; defer.resolve(cubes.length); @@ -75,11 +70,10 @@ KylinApp }; $scope.$watch('project.selectedProject', function (newValue, oldValue) { - //exclude when refresh page oldValue=null,first time set value for project (will have page auto reload ,incase duplicate) oldvalue is null - if(newValue){ + if(newValue!=oldValue||newValue==null){ $scope.cubes=[]; $scope.reload(); - } + } }); $scope.reload = function () { diff --git a/webapp/app/js/controllers/job.js b/webapp/app/js/controllers/job.js index e2975432a..d130f457e 100644 --- a/webapp/app/js/controllers/job.js +++ b/webapp/app/js/controllers/job.js @@ -1,7 +1,7 @@ 'use strict'; KylinApp - .controller('JobCtrl', function ($scope, $q, $routeParams, $interval, $modal, ProjectService, MessageService, JobService,SweetAlert,loadingRequest) { + .controller('JobCtrl', function ($scope, $q, $routeParams, $interval, $modal, ProjectService, MessageService, JobService,SweetAlert,loadingRequest,UserService) { $scope.cubeName = null; $scope.jobs = {}; $scope.projects = []; @@ -31,6 +31,8 @@ KylinApp } }; + + // projectName from page ctrl $scope.state = {loading: false, refreshing: false, filterAttr: 'last_modified', filterReverse: true, reverseColumn: 'last_modified', projectName:$scope.project.selectedProject}; @@ -41,8 +43,8 @@ KylinApp }); $scope.list = function (offset, limit) { - if(!$scope.project.selectedProject){ - return; + if(!$scope.project.projects.length){ + return []; } offset = (!!offset) ? offset : 0; var selectedJob = null; @@ -94,7 +96,7 @@ KylinApp $scope.$watch('project.selectedProject', function (newValue, oldValue) { - if(newValue){ + if(newValue!=oldValue||newValue==null){ $scope.jobs={}; $scope.state.projectName = newValue; $scope.reload(); @@ -132,9 +134,10 @@ KylinApp }); } + $scope.cancel = function (job) { SweetAlert.swal({ - title: 'Confirm', + title: '', text: 'Are you sure to discard the job?', type: '', showCancelButton: true, @@ -145,10 +148,13 @@ KylinApp loadingRequest.show(); JobService.cancel({jobId: job.uuid}, {}, function (job) { loadingRequest.hide(); - $scope.jobs[job.uuid] = job; - if (angular.isDefined($scope.state.selectedJob)) { - $scope.state.selectedJob = $scope.jobs[ $scope.state.selectedJob.uuid]; - } + $scope.safeApply(function() { + $scope.jobs[job.uuid] = job; + if (angular.isDefined($scope.state.selectedJob)) { + $scope.state.selectedJob = $scope.jobs[ $scope.state.selectedJob.uuid]; + } + + }); SweetAlert.swal('Success!', 'Job has been discarded successfully!', 'success'); },function(e){ loadingRequest.hide(); diff --git a/webapp/app/js/controllers/page.js b/webapp/app/js/controllers/page.js index c34ca7b48..cdb4b3e40 100644 --- a/webapp/app/js/controllers/page.js +++ b/webapp/app/js/controllers/page.js @@ -28,7 +28,6 @@ KylinApp.controller('PageCtrl', function ($scope, $q, AccessService,$modal, $loc $location.path('/login'); console.debug("Logout Completed."); - $scope.project.selectedProject = null; }).error(function () { UserService.setCurUser({}); $scope.username = $scope.password = null; @@ -123,17 +122,24 @@ KylinApp.controller('PageCtrl', function ($scope, $q, AccessService,$modal, $loc selectedProject: null }; + $scope.projectVisible = function(project){ + $log.info(project); + return project!='-- Select All --'; + } + ProjectService.list({}, function (projects) { angular.forEach(projects, function(project, index){ $scope.project.projects.push(project.name); }); + $scope.project.projects.sort(); + var absUrl = $location.absUrl(); var projectInCookie = $cookieStore.get("project"); if(absUrl.indexOf("/login")==-1){ - $scope.project.selectedProject=$scope.project.selectedProject!=null?$scope.project.selectedProject:projectInCookie!=null?projectInCookie:$scope.project.projects[0] - }else{ $scope.project.selectedProject=projectInCookie!=null?projectInCookie:null; + }else{ + $scope.project.selectedProject=$scope.project.selectedProject!=null?$scope.project.selectedProject:projectInCookie!=null?projectInCookie:$scope.project.projects[0]; } }); @@ -154,13 +160,31 @@ KylinApp.controller('PageCtrl', function ($scope, $q, AccessService,$modal, $loc $scope.$watch('project.selectedProject', function (newValue, oldValue) { - if(newValue){ - $log.log("project updated in page controller"); + if(newValue!=oldValue){ + $log.log("project updated in page controller,from:"+oldValue+" To:"+newValue); $cookieStore.put("project",$scope.project.selectedProject); } }); + /* + *global method for all scope to use + * */ + + //update scope value to view + $scope.safeApply = function(fn) { + var phase = this.$root.$$phase; + if(phase == '$apply' || phase == '$digest') { + if(fn && (typeof(fn) === 'function')) { + fn(); + } + } else { + this.$apply(fn); + } + }; + + + }); var projCtrl = function ($scope, $modalInstance, ProjectService, MessageService, projects, project,SweetAlert) { @@ -181,6 +205,7 @@ var projCtrl = function ($scope, $modalInstance, ProjectService, MessageService, $scope.createOrUpdate = function () { if ($scope.state.isEdit) { + var requestBody = { formerProjectName: $scope.state.oldProjName, newProjectName: $scope.proj.name, @@ -223,4 +248,6 @@ var projCtrl = function ($scope, $modalInstance, ProjectService, MessageService, $modalInstance.dismiss('cancel'); }; + }; + diff --git a/webapp/app/js/controllers/projects.js b/webapp/app/js/controllers/projects.js index ff4418bea..119e6db92 100644 --- a/webapp/app/js/controllers/projects.js +++ b/webapp/app/js/controllers/projects.js @@ -1,7 +1,7 @@ 'use strict'; KylinApp - .controller('ProjectCtrl', function ($scope, $modal, $q, ProjectService, MessageService,SweetAlert) { + .controller('ProjectCtrl', function ($scope, $modal, $q, ProjectService, MessageService,SweetAlert,$log) { $scope.projects = []; $scope.loading = false; $scope.theaditems = [ diff --git a/webapp/app/js/controllers/sourceMeta.js b/webapp/app/js/controllers/sourceMeta.js index 2f1ea4cbb..bb8b8af6d 100644 --- a/webapp/app/js/controllers/sourceMeta.js +++ b/webapp/app/js/controllers/sourceMeta.js @@ -97,9 +97,9 @@ KylinApp }; $scope.$watch('project.selectedProject', function (newValue, oldValue) { - if(newValue){ - $scope.aceSrcTbLoaded(); - } +// will load table when enter this page,null or not + $scope.aceSrcTbLoaded(); + }); $scope.$watch('hiveTbLoad.status', function (newValue, oldValue) { @@ -109,12 +109,12 @@ KylinApp }); - $scope.showSelected = function (table) { - if (table.uuid) { - $scope.selectedSrcTable = table; + $scope.showSelected = function (obj) { + if (obj.uuid) { + $scope.selectedSrcTable = obj; } - else { - $scope.selectedSrcTable.selectedSrcColumn = table; + else if(obj.datatype) { + $scope.selectedSrcTable.selectedSrcColumn = obj; } }; @@ -159,6 +159,12 @@ KylinApp SweetAlert.swal('','Please input table(s) you want to synchronize.', 'info'); return; } + + if(!$scope.projectName){ + SweetAlert.swal('','Please choose your project first!.', 'info'); + return; + } + $scope.cancel(); loadingRequest.show(); TableService.loadHiveTable({tableName: $scope.tableNames,action:projectName}, {}, function (result) { diff --git a/webapp/app/less/animation.less b/webapp/app/less/animation.less new file mode 100644 index 000000000..faba2263d --- /dev/null +++ b/webapp/app/less/animation.less @@ -0,0 +1,29 @@ +/** animation for data model **/ +.data-model-animation.ng-enter { + -webkit-animation: fadeInRight 1s; + -moz-animation: fadeInRight 1s; + -ms-animation: fadeInRight 1s; + animation: fadeInRight 1s; +} + +.data-model-animation.ng-leave { + -webkit-animation: fadeOutRight 1s; + -moz-animation: fadeOutRight 1s; + -ms-animation: fadeOutRight 1s; + animation: fadeOutRight 1s; +} + +.dimension-options-animate.ng-hide-remove { + -webkit-animation: fadeInRight 1s; + -moz-animation: fadeInRight 1s; + -ms-animation: fadeInRight 1s; + animation: fadeInRight 1s; +} + +.dimension-options-animate.ng-hide-add { + -webkit-animation: fadeOutRight 1s; + -moz-animation: fadeOutRight 1s; + -ms-animation: fadeOutRight 1s; + animation: fadeOutRight 1s; + display: block !important; +} diff --git a/webapp/app/less/app.less b/webapp/app/less/app.less index 048793624..f60a69850 100644 --- a/webapp/app/less/app.less +++ b/webapp/app/less/app.less @@ -364,6 +364,11 @@ input.ng-invalid-required { border: 2px solid lightblue; } +input.ng-invalid-required:after { + content: "*"; + color: red; +} + .nav-pills>li { cursor: pointer; background-color: #094868; @@ -390,7 +395,12 @@ treecontrol > ul { treecontrol > ul > li { padding-left: 0; } - +treecontrol li .tree-label{ + cursor: default; +} +treecontrol li treeitem .tree-label{ + cursor: pointer; +} treecontrol.tree-light li.tree-collapsed i { padding: 1px 10px; background: url("../image/database.png") no-repeat; @@ -536,3 +546,48 @@ ul.messenger .messenger-message-inner { .load-hive-metawrapper input[type="radio"]{ margin-left:0px !important; } + +/** cube data model and dimensions **/ +.table-name-underline { + margin: 0 0 5px 0; + border-bottom: 1px dashed #ddd; + padding-bottom: 3px; +} + +.columns-region, .dimensions-region { + max-height: 400px; + overflow: auto; +} + +.dimensions-region li { + cursor: pointer; +} + +.dimensions-region li:hover { + background: #f6f6f6; +} + +.dimensions-region li.active { + background-color: lightskyblue; +} + +/** cube dimension hierarchy sortable styles **/ +.hierarchy-item, .hierarchy-item-placeholder { + margin: 1px 2px 5px 1px; + border-radius: 5px; + background-color: #ffffff; +} + +.hierarchy-item { + border: solid 1px darkgrey; + cursor: move; +} + +.hierarchy-item-placeholder { + border: 1px dotted #0000ff !important; +} + +.dimensions-conflict-area { + max-height: 300px; + overflow-y: auto; +} diff --git a/webapp/app/less/build.less b/webapp/app/less/build.less index 4256a9110..a8d0bb298 100644 --- a/webapp/app/less/build.less +++ b/webapp/app/less/build.less @@ -3,3 +3,4 @@ @import 'home.less'; @import 'app.less'; @import 'component.less'; +@import 'animation.less'; diff --git a/webapp/app/partials/cubeDesigner/advanced_settings.html b/webapp/app/partials/cubeDesigner/advanced_settings.html index d1c6dc51e..7db30775f 100644 --- a/webapp/app/partials/cubeDesigner/advanced_settings.html +++ b/webapp/app/partials/cubeDesigner/advanced_settings.html @@ -1,4 +1,29 @@
+ +
+ + + + + +
+ +
+
+ +
+ + {{cubeMetaFrame.capacity}} +
+
+
+
+ +

Aggregation Groups

+ + + + +
+
+ +
+ + {{cubeMetaFrame.model.fact_table}} +
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + +
IDTable NameJoin TypeJoin ConditionActions
+ {{$index + 1}} + + {{lookup.table}} + + {{lookup.join.type}} + +
    +
  • + {{lookup.table + '.' + pk}} = {{cubeMetaFrame.model.fact_table + '.' + lookup.join.foreign_key[$index]}} +
  • +
+
+ + + + +
+
+ + + + + +
diff --git a/webapp/app/partials/cubeDesigner/dimensions.html b/webapp/app/partials/cubeDesigner/dimensions.html index c4a6d0b4b..65c27081e 100644 --- a/webapp/app/partials/cubeDesigner/dimensions.html +++ b/webapp/app/partials/cubeDesigner/dimensions.html @@ -1,83 +1,90 @@ - -
-
- -
- - {{cubeMetaFrame.fact_table}} -
-
-
+
-